mirror of
https://github.com/nextapps-de/flexsearch.git
synced 2025-09-25 12:58:59 +02:00
503 lines
14 KiB
JavaScript
503 lines
14 KiB
JavaScript
/**!
|
|
* FlexSearch.js
|
|
* Author and Copyright: Thomas Wilkerling
|
|
* Licence: Apache-2.0
|
|
* Hosted by Nextapps GmbH
|
|
* https://github.com/nextapps-de/flexsearch
|
|
*/
|
|
|
|
import { IndexOptions, DocumentOptions, DocumentDescriptor, FieldOptions, StoreOptions, EncoderOptions } from "./type.js";
|
|
import StorageInterface from "./db/interface.js";
|
|
import Index from "./index.js";
|
|
import WorkerIndex from "./worker.js";
|
|
import Encoder from "./encoder.js";
|
|
import Cache, { searchCache } from "./cache.js";
|
|
import { is_string, is_object, parse_simple } from "./common.js";
|
|
import apply_async from "./async.js";
|
|
import { exportDocument, importDocument } from "./serialize.js";
|
|
import { KeystoreMap, KeystoreSet } from "./keystore.js";
|
|
import "./document/add.js";
|
|
import "./document/search.js";
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {!DocumentOptions} options
|
|
* @return {Document|Promise<Document>}
|
|
* @this {Document}
|
|
*/
|
|
|
|
export default function Document(options) {
|
|
|
|
if (!this || this.constructor !== Document) {
|
|
return new Document(options);
|
|
}
|
|
|
|
const document = /** @type DocumentDescriptor */options.document || options.doc || options;
|
|
let tmp, keystore;
|
|
|
|
this.tree = [];
|
|
this.field = [];
|
|
this.marker = [];
|
|
this.key = (tmp = document.key || document.id) && parse_tree(tmp, this.marker) || "id";
|
|
|
|
keystore = options.keystore || 0;
|
|
keystore && (this.keystore = keystore);
|
|
this.fastupdate = !!options.fastupdate;
|
|
// Shared Registry
|
|
this.reg = this.fastupdate && !options.worker && !options.db ? keystore && /* tag? */ /* stringify */ /* stringify */ /* single param */ /* skip update: */ /* append: */ /* skip update: */ /* skip_update: */ /* skip deletion */!0 /*await rows.hasNext()*/ /*await rows.hasNext()*/ /*await rows.hasNext()*/ ? new KeystoreMap(keystore) : new Map() : keystore && !0 ? new KeystoreSet(keystore) : new Set();
|
|
|
|
// todo support custom filter function
|
|
this.storetree = (tmp = document.store || null) && tmp && !0 !== tmp && [];
|
|
this.store = tmp && (keystore && !0 ? new KeystoreMap(keystore) : new Map());
|
|
|
|
this.cache = (tmp = options.cache || null) && new Cache(tmp);
|
|
// do not apply cache again for the indexes since .searchCache()
|
|
// is just a wrapper over .search()
|
|
options.cache = /* suggest */ /* append: */ /* enrich */!1;
|
|
|
|
this.worker = options.worker || !1;
|
|
|
|
this.priority = options.priority || 4;
|
|
|
|
/**
|
|
* @type {Map<string, Index>}
|
|
*/
|
|
this.index = parse_descriptor.call(this, options, document);
|
|
|
|
this.tag = null;
|
|
// TODO case-insensitive tags?
|
|
if (tmp = document.tag) {
|
|
if ("string" == typeof tmp) {
|
|
tmp = [tmp];
|
|
}
|
|
if (tmp.length) {
|
|
this.tag = new Map();
|
|
this.tagtree = [];
|
|
this.tagfield = [];
|
|
for (let i = 0, params, field; i < tmp.length; i++) {
|
|
params = tmp[i];
|
|
field = params.field || params;
|
|
if (!field) {
|
|
throw new Error("The tag field from the document descriptor is undefined.");
|
|
}
|
|
if (params.custom) {
|
|
this.tagtree[i] = params.custom;
|
|
} else {
|
|
this.tagtree[i] = parse_tree(field, this.marker);
|
|
if (params.filter) {
|
|
if ("string" == typeof this.tagtree[i]) {
|
|
// it needs an object to put a property to it
|
|
this.tagtree[i] = new String(this.tagtree[i]);
|
|
}
|
|
this.tagtree[i]._filter = params.filter;
|
|
}
|
|
}
|
|
// the tag fields needs to be hold by indices
|
|
this.tagfield[i] = field;
|
|
this.tag.set(field, new Map());
|
|
}
|
|
}
|
|
}
|
|
|
|
// resolve worker promises and swap instances
|
|
if (this.worker) {
|
|
this.fastupdate = !1;
|
|
const promises = [];
|
|
for (const index of this.index.values()) {
|
|
index.then && promises.push(index);
|
|
}
|
|
if (promises.length) {
|
|
const self = this;
|
|
return Promise.all(promises).then(function (result) {
|
|
const encoder_last = new Map();
|
|
let count = 0;
|
|
for (const item of self.index.entries()) {
|
|
const key = /** @type {string} */item[0];
|
|
let index = item[1];
|
|
if (index.then) {
|
|
// make encoder available for result highlighting
|
|
let opt = promises[count].encoder || {},
|
|
encoder = encoder_last.get(opt);
|
|
// handle shared encoders
|
|
|
|
if (!encoder) {
|
|
encoder = opt.encode ? opt : new Encoder(opt);
|
|
encoder_last.set(opt, encoder);
|
|
}
|
|
index = result[count];
|
|
index.encoder = encoder;
|
|
self.index.set(key, index);
|
|
count++;
|
|
}
|
|
}
|
|
return self;
|
|
});
|
|
}
|
|
} else {
|
|
if (options.db) {
|
|
this.fastupdate = !1;
|
|
// actually it can be awaited on "await index.db"
|
|
this.mount(options.db);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!StorageInterface} db
|
|
* @return {Promise<void>}
|
|
*/
|
|
Document.prototype.mount = function (db) {
|
|
if (this.worker) {
|
|
throw new Error("You can't use Worker-Indexes on a persistent model. That would be useless, since each of the persistent model acts like Worker-Index by default (Master/Slave).");
|
|
}
|
|
|
|
|
|
let fields = this.field;
|
|
|
|
if (this.tag) {
|
|
// tag indexes are referenced by field
|
|
// move tags to their field indexes respectively
|
|
for (let i = 0, field; i < this.tagfield.length; i++) {
|
|
field = this.tagfield[i];
|
|
let index; // = this.index.get(field);
|
|
//if(!index){
|
|
// create raw index when not exists
|
|
this.index.set(field, index = new Index( /** @type IndexOptions */{}, this.reg));
|
|
// copy and push to the field selection
|
|
if (fields === this.field) {
|
|
fields = fields.slice(0);
|
|
}
|
|
// tag indexes also needs to be upgraded to db instances
|
|
fields.push(field);
|
|
//}
|
|
// assign reference
|
|
index.tag = this.tag.get(field);
|
|
}
|
|
}
|
|
|
|
const promises = [],
|
|
config = {
|
|
db: db.db,
|
|
type: db.type,
|
|
fastupdate: db.fastupdate
|
|
};
|
|
|
|
|
|
// upgrade all indexes to db instances
|
|
for (let i = 0, index, field; i < fields.length; i++) {
|
|
config.field = field = fields[i];
|
|
index = this.index.get(field);
|
|
const dbi = new db.constructor(db.id, config);
|
|
// take over the storage id
|
|
dbi.id = db.id;
|
|
promises[i] = dbi.mount(index);
|
|
// add an identification property
|
|
index.document = !0;
|
|
if (i) {
|
|
// the register has to export just one time
|
|
// also it's needed by the index for ID contain check
|
|
index.bypass = !0;
|
|
} else {
|
|
// the datastore has to export one time
|
|
index.store = this.store;
|
|
}
|
|
}
|
|
|
|
const self = this;
|
|
return this.db = Promise.all(promises).then(function () {
|
|
self.db = !0;
|
|
});
|
|
};
|
|
|
|
Document.prototype.commit = async function (replace, append) {
|
|
// parallel:
|
|
const promises = [];
|
|
for (const index of this.index.values()) {
|
|
promises.push(index.commit(replace, append));
|
|
}
|
|
await Promise.all(promises);
|
|
this.reg.clear();
|
|
// queued:
|
|
// for(const index of this.index.values()){
|
|
// await index.db.commit(index, replace, append);
|
|
// }
|
|
// this.reg.clear();
|
|
};
|
|
|
|
Document.prototype.destroy = function () {
|
|
const promises = [];
|
|
for (const idx of this.index.values()) {
|
|
promises.push(idx.destroy());
|
|
}
|
|
return Promise.all(promises);
|
|
};
|
|
|
|
/**
|
|
* @this {Document}
|
|
*/
|
|
|
|
function parse_descriptor(options, document) {
|
|
|
|
const index = new Map();
|
|
let field = document.index || document.field || document;
|
|
|
|
if (is_string(field)) {
|
|
field = [field];
|
|
}
|
|
|
|
for (let i = 0, key, opt; i < field.length; i++) {
|
|
|
|
key = field[i];
|
|
|
|
if (!is_string(key)) {
|
|
opt = key;
|
|
key = key.field;
|
|
}
|
|
|
|
opt = /** @type IndexOptions */is_object(opt) ? Object.assign({}, options, opt) : options;
|
|
|
|
if (this.worker) {
|
|
const worker = new WorkerIndex(opt);
|
|
if (worker) {
|
|
// assign encoder for result highlighting
|
|
worker.encoder = opt.encoder;
|
|
// worker could be a promise
|
|
// it needs to be resolved and swapped later
|
|
index.set(key, worker);
|
|
} else {
|
|
// fallback when not supported
|
|
this.worker = !1;
|
|
}
|
|
}
|
|
|
|
if (!this.worker) {
|
|
index.set(key, new Index( /** @type IndexOptions */opt, this.reg));
|
|
}
|
|
|
|
if (opt.custom) {
|
|
this.tree[i] = opt.custom;
|
|
} else {
|
|
this.tree[i] = parse_tree(key, this.marker);
|
|
if (opt.filter) {
|
|
if ("string" == typeof this.tree[i]) {
|
|
// it needs an object to put a property to it
|
|
this.tree[i] = new String(this.tree[i]);
|
|
}
|
|
this.tree[i]._filter = opt.filter;
|
|
}
|
|
}
|
|
|
|
this.field[i] = key;
|
|
}
|
|
|
|
if (this.storetree) {
|
|
|
|
let stores = document.store;
|
|
if (is_string(stores)) stores = [stores];
|
|
|
|
for (let i = 0, store, field; i < stores.length; i++) {
|
|
store = /** @type Array<StoreOptions> */stores[i];
|
|
field = store.field || store;
|
|
if (store.custom) {
|
|
this.storetree[i] = store.custom;
|
|
store.custom._field = field;
|
|
} else {
|
|
this.storetree[i] = parse_tree(field, this.marker);
|
|
if (store.filter) {
|
|
if ("string" == typeof this.storetree[i]) {
|
|
// it needs an object to put a property to it
|
|
this.storetree[i] = new String(this.storetree[i]);
|
|
}
|
|
this.storetree[i]._filter = store.filter;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
function parse_tree(key, marker) {
|
|
|
|
const tree = key.split(":");
|
|
let count = 0;
|
|
|
|
for (let i = 0; i < tree.length; i++) {
|
|
key = tree[i];
|
|
// todo apply some indexes e.g. [0], [-1], [0-2]
|
|
if ("]" === key[key.length - 1]) {
|
|
key = key.substring(0, key.length - 2);
|
|
if (key) {
|
|
marker[count] = !0;
|
|
}
|
|
}
|
|
if (key) {
|
|
tree[count++] = key;
|
|
}
|
|
}
|
|
|
|
if (count < tree.length) {
|
|
tree.length = count;
|
|
}
|
|
|
|
return 1 < count ? tree : tree[0];
|
|
}
|
|
|
|
/**
|
|
* @param {!number|Object} id
|
|
* @param {!Object} content
|
|
* @return {Document|Promise<Document>}
|
|
*/
|
|
Document.prototype.append = function (id, content) {
|
|
return this.add(id, content, !0);
|
|
};
|
|
|
|
/**
|
|
* @param {!number|Object} id
|
|
* @param {!Object} content
|
|
* @return {Document|Promise<Document>}
|
|
*/
|
|
Document.prototype.update = function (id, content) {
|
|
return this.remove(id).add(id, content);
|
|
};
|
|
|
|
/**
|
|
* @param {!number|Object} id
|
|
* @return {Document|Promise<Document>}
|
|
*/
|
|
Document.prototype.remove = function (id) {
|
|
|
|
if (is_object(id)) {
|
|
id = parse_simple(id, this.key);
|
|
}
|
|
|
|
for (const index of this.index.values()) {
|
|
index.remove(id, !0);
|
|
}
|
|
|
|
if (this.reg.has(id)) {
|
|
|
|
if (this.tag) {
|
|
// when fastupdate was enabled all ids are already removed
|
|
if (!this.fastupdate) {
|
|
for (let field of this.tag.values()) {
|
|
for (let item of field) {
|
|
const tag = item[0],
|
|
ids = item[1],
|
|
pos = ids.indexOf(id);
|
|
|
|
if (-1 < pos) {
|
|
1 < ids.length ? ids.splice(pos, 1) : field.delete(tag);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.store) {
|
|
this.store.delete(id);
|
|
}
|
|
|
|
this.reg.delete(id);
|
|
}
|
|
|
|
// the cache could be used outside the InMemory store
|
|
if (this.cache) {
|
|
this.cache.remove(id);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
Document.prototype.clear = function () {
|
|
|
|
const promises = [];
|
|
|
|
for (const index of this.index.values()) {
|
|
// db index will add clear task
|
|
const promise = index.clear();
|
|
// worker indexes will return promises
|
|
if (promise.then) {
|
|
promises.push(promise);
|
|
}
|
|
}
|
|
|
|
if (this.tag) {
|
|
for (const tags of this.tag.values()) {
|
|
tags.clear();
|
|
}
|
|
}
|
|
|
|
if (this.store) {
|
|
this.store.clear();
|
|
}
|
|
|
|
if (this.cache) {
|
|
this.cache.clear();
|
|
}
|
|
|
|
return promises.length ? Promise.all(promises) : this;
|
|
};
|
|
|
|
/**
|
|
* @param {number|string} id
|
|
* @return {boolean|Promise<boolean>}
|
|
*/
|
|
Document.prototype.contain = function (id) {
|
|
|
|
if (this.db) {
|
|
return this.index.get(this.field[0]).db.has(id);
|
|
}
|
|
|
|
return this.reg.has(id);
|
|
};
|
|
|
|
Document.prototype.cleanup = function () {
|
|
|
|
for (const index of this.index.values()) {
|
|
index.cleanup();
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* @param {number|string} id
|
|
* @return {Object}
|
|
*/
|
|
Document.prototype.get = function (id) {
|
|
|
|
if (this.db) {
|
|
return this.index.get(this.field[0]).db.enrich(id).then(function (result) {
|
|
return result[0] && result[0].doc || null;
|
|
});
|
|
}
|
|
|
|
return this.store.get(id) || null;
|
|
};
|
|
|
|
/**
|
|
* @param {number|string|Object} id
|
|
* @param {Object} data
|
|
* @return {Document}
|
|
*/
|
|
Document.prototype.set = function (id, data) {
|
|
|
|
if ("object" == typeof id) {
|
|
data = id;
|
|
id = parse_simple(data, this.key);
|
|
}
|
|
|
|
this.store.set(id, data);
|
|
return this;
|
|
};
|
|
|
|
// todo mo
|
|
Document.prototype.searchCache = searchCache;
|
|
|
|
|
|
Document.prototype.export = exportDocument;
|
|
Document.prototype.import = importDocument;
|
|
|
|
|
|
apply_async(Document.prototype); |