UNPKG

gatsby-remark-related-posts

Version:

Calculate the similarity between posts by tf-idf and Cosine similarity.

253 lines (252 loc) 11.9 kB
var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _this = this; Object.defineProperty(exports, "__esModule", { value: true }); exports.onPostBootstrap = exports.createSchemaCustomization = void 0; var kuromoji_1 = __importDefault(require("kuromoji")); var natural_1 = require("natural"); var path_1 = __importDefault(require("path")); var crypto_1 = __importDefault(require("crypto")); var computeCosineSimilarity = require('compute-cosine-similarity'); // settings var default_option = { doc_lang: 'en', target_node: 'MarkdownRemark', getMarkdown: function (node) { return node.rawMarkdownBody; }, each_bow_size: 30, }; // utils var md5 = function (str) { var md5 = crypto_1.default.createHash('md5'); return md5.update(str, 'binary').digest('hex'); }; var calcVectorSimilarity = function (v1, v2) { if (v1.length !== v2.length) throw new Error("Both vector's size must be equal"); return computeCosineSimilarity(v1, v2); }; var vector_similarity_memo = new Map(); var getMemorizedVectorSimilarity = function (v1, v2) { var id = v1.id < v2.id ? "".concat(v1.id, " ").concat(v2.id) : "".concat(v2.id, " ").concat(v1.id); var memorized_similarity = vector_similarity_memo.get(id); if (memorized_similarity !== undefined) return memorized_similarity; var similarity = calcVectorSimilarity(v1.vector, v2.vector); vector_similarity_memo.set(id, similarity); return similarity; }; var getRelatedPosts = function (id, bow_vectors) { var vector = bow_vectors.get(id); if (vector === undefined) return []; var vector_node = { id: id, vector: vector, }; return Array.from(bow_vectors.entries()) .sort(function (x, y) { var vector_x = { id: x[0], vector: x[1], }; var vector_y = { id: y[0], vector: y[1], }; return (getMemorizedVectorSimilarity(vector_y, vector_node) - getMemorizedVectorSimilarity(vector_x, vector_node)); }) .map(function (x) { return x[0]; }); }; var getTextFromMarkdown = function (markdown) { return markdown .replace(/```[\s\S]+?```/g, '') .replace(/---[\s\S]+?---/g, '') .replace(/\$[\s\S]+?\$/g, '') .replace(/\$\$[\s\S]+?\$\$/g, '') .replace(/^\|.*\|$/gm, '') .replace(/<.+?>/g, '') .replace(/http[^ ]+/g, '') .replace(/[\#\!\(\)\*\_\[\]\|\=\>\+\`\:\-]/g, ''); }; var getKuromojiTokenizer = function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, new Promise(function (resolve, reject) { kuromoji_1.default .builder({ dicPath: path_1.default.join(path_1.default.dirname(require.resolve('kuromoji')), '../dict'), }) .build(function (err, tokenizer) { if (err) reject(); resolve(tokenizer); }); })]; }); }); }; var kuromoji_tokenizer = null; var getSpaceSeparatedDoc = { en: function (doc) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, doc.toLowerCase().split(' ')]; }); }); }, ja: function (doc) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: if (!(kuromoji_tokenizer === null)) return [3 /*break*/, 2]; return [4 /*yield*/, getKuromojiTokenizer()]; case 1: kuromoji_tokenizer = _a.sent(); _a.label = 2; case 2: return [2 /*return*/, kuromoji_tokenizer .tokenize(doc) .filter(function (x) { return x.pos === '名詞' && ['一般', '固有名詞'].indexOf(x.pos_detail_1) !== -1; }) .map(function (x) { return (x.basic_form !== '*' ? x.basic_form : x.surface_form); })]; } }); }); }, }; // gatsby api var createSchemaCustomization = function (_a, user_option) { var actions = _a.actions; var option = __assign(__assign({}, default_option), user_option); actions.createTypes("\n type related".concat(option.target_node, "s implements Node {\n posts: [").concat(option.target_node, "]\n }\n ")); }; exports.createSchemaCustomization = createSchemaCustomization; var onPostBootstrap = function (_a, user_option) { var actions = _a.actions, getNode = _a.getNode, getNodesByType = _a.getNodesByType, createNodeId = _a.createNodeId, reporter = _a.reporter, cache = _a.cache; return __awaiter(_this, void 0, void 0, function () { var option, nodes, docs, tfidf, _i, docs_1, doc, key, cached_ssd, ssd, doc_terms, all_keywords, tfidf_map_for_each_doc, bow_vectors; return __generator(this, function (_b) { switch (_b.label) { case 0: option = __assign(__assign({}, default_option), user_option); nodes = getNodesByType(option.target_node); docs = nodes.map(function (node) { return ({ id: node.id, text: option.getMarkdown(node), }); }); tfidf = new natural_1.TfIdf(); _i = 0, docs_1 = docs; _b.label = 1; case 1: if (!(_i < docs_1.length)) return [3 /*break*/, 6]; doc = docs_1[_i]; key = "related-posts-ssd-".concat(md5(doc.text)); return [4 /*yield*/, cache.get(key)]; case 2: cached_ssd = _b.sent(); if (cached_ssd !== undefined) { tfidf.addDocument(cached_ssd); return [3 /*break*/, 5]; } return [4 /*yield*/, getSpaceSeparatedDoc[option.doc_lang](getTextFromMarkdown(doc.text))]; case 3: ssd = _b.sent(); tfidf.addDocument(ssd); return [4 /*yield*/, cache.set(key, ssd)]; case 4: _b.sent(); _b.label = 5; case 5: _i++; return [3 /*break*/, 1]; case 6: doc_terms = docs.map(function (_, i) { return tfidf.listTerms(i) .map(function (x) { return (__assign(__assign({}, x), { tfidf: x.tf * x.idf })); }) .sort(function (x, y) { return y.tfidf - x.tfidf; }); }); all_keywords = new Set(); tfidf_map_for_each_doc = []; doc_terms.forEach(function (x, i) { tfidf_map_for_each_doc[i] = new Map(); x.slice(0, option.each_bow_size).forEach(function (x) { all_keywords.add(x.term); tfidf_map_for_each_doc[i].set(x.term, x.tfidf); }); }); bow_vectors = new Map(); docs.forEach(function (x, i) { if (bow_vectors === null) return; bow_vectors.set(x.id, Array.from(all_keywords) .map(function (x) { return tfidf_map_for_each_doc[i].get(x); }) .map(function (x) { return (x === undefined ? 0 : x); })); }); reporter.info("[related-posts] bow vectors generated, dimention: ".concat(all_keywords.size)); // create related nodes nodes.forEach(function (node) { var related_nodes = getRelatedPosts(node.id, bow_vectors) .slice(1) .map(function (id) { return getNode(id); }); var digest = "".concat(node.id, " >>> related").concat(option.target_node, "s"); actions.createNode({ id: createNodeId(digest), parent: node.id, internal: { type: "related".concat(option.target_node, "s"), contentDigest: digest, }, posts: related_nodes, }); }); return [2 /*return*/]; } }); }); }; exports.onPostBootstrap = onPostBootstrap;