copious-little-searcher
Version:
A small Search Server for finding assets pubished on a particular blog
466 lines (422 loc) • 14.1 kB
JavaScript
//
const {Registry} = require('copious-registry')
const {FileOperationsCache} = require('extra-file-class')
//
const {QueryResult} = require('./lib/defaut_queries')
//
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
/**
* Searching
*/
class Searching extends Registry {
//
constructor(conf,QueryInterfaceClass) {
//
super(conf)
//
this.functional_evaluator = false
if ( conf.functional_evaluator !== undefined ) {
try {
this.functional_evaluator = require(conf.functional_evaluator)
} catch (e) {
console.log(e)
}
}
//
this.fos = new FileOperationsCache(conf)
//
this.local_active_searches = {}
//
this.shrinkage = conf.shrinkage
this.backup_file = conf.search_backup_file
//
this.identifier_key = "_tracking"
if ( conf.repo_id_field ) {
this.identifier_key = conf.repo_id_field
}
//
this.QInterfaceClass = ((QueryInterfaceClass !== undefined) ? QueryInterfaceClass : QueryResult)
//
this._search_functions = {}
if ( typeof conf.search_function_table === 'object' ) {
this._search_functions = conf.search_function_table
for ( let ky in this._search_functions ) {
let ff = this._search_functions[ky]
if ( typeof ff !== 'function' ) {
delete this._search_functions[ky]
}
}
}
}
/**
*
* @param {*} query
* @returns
*/
query_hasher(query) {
if ( typeof this.hasher === 'function' ) {
let s = query.trim()
return this.hasher(s)
}
return "nothing"
}
/**
* set_query_interface_class
*
* Changes the query parsing and match control class.
* Caution should be used. Most likely a user interface will be using a syntax that complies with
* another query parser. It may be best to expand on existing classes in use if not planning an overhaul
* to user interfaces.
*
* @param {string} class_module
*/
set_query_interface_class(class_module) {
try {
this.QInterfaceClass = require(class_module)
} catch (e) {
console.log(e)
}
}
/**
*
* run_query
*
* creates new queries and then runs it agains the loaded index already in store.
*
* Hands query parsing to the QInterfaceClass
* Return the query object and the query string.
*
* The data found by the query (refs) is attached to the query object in a list.
*
* @param {string} query
* @returns Pair<QueryClass,string>
*/
async run_query(query) {
let q = new this.QInterfaceClass(query,this.functional_evaluator)
let [match_text,orderby] = q.parts()
let data = this._run_query(match_text,orderby,q)
if ( data.length ) {
q.set_data(data)
return q
}
return false
}
/**
* get_search
*
* Givent that a query has run, this method returns retuns some part of the data it keeps.
* The query is found in the table of queries (ones already run) by the query string
* that acts as its key.
*
* Expects the query to return a descriptor (determined by the query class)
*
* For example:
*
*
```{
"data" : returned_data, // the current small bucket set of data to fit the user view
"length" : returned_data.length,
"offset" : offset,
"count" : count // number of possible results to view
}
```
*
* @param {string} query
* @param {integer} offset - offset into the data list (start of data to be returned)
* @param {integer} box_count - the number of elements to be returned
* @returns object
*/
async get_search(query,offset,box_count) {
let qhash = await this.query_hasher(query)
//
// try to find a search a query result object for this query
//
let qry_instance = this.local_active_searches[qhash]
if ( qry_instance !== undefined ) {
let data_descr = qry_instance.access(offset,box_count)
return data_descr
} else {
// could not find one so create a new one...
let q_obj= await this.run_query(query)
if ( q_obj === false ) return []
else {
this.local_active_searches[qhash] = q_obj // THE NORMALIZED QUERY ACTS ACTS AS A KEY TO THE QUERY OBJECT
let data_descr = q_obj.access(offset,box_count)
return data_descr
}
}
}
search_one_all_files(field,match_text) {
let list = this.global_file_list_by["update_date"] // return an iterable
for ( let item of list ) {
if ( item[field] === match_text ) {
return [ item ]
}
}
return []
}
search_by_field_all_files(field,match_text) {
let results = this.global_file_list.filter((ob) => {
return obj[field] === match_text
})
return results
}
search_by_function_all_files(funcdef) {
let results = this.global_file_list.filter((ob) => {
return funcdef(obj)
})
return results
}
/**
* attempt_join_searches
*
* Attempts to run the query on the new object to see if it can be added to query result lists.
*
* injest an element into searchs... look at all the searches and add it to the query list calling q.inject
*
* @param {*} f_obj
*/
attempt_join_searches(f_obj) {
let searches = this.local_active_searches
for ( let query in searches ) {
let q = searches[query]
let [match_text,orderby] = q.parts()
if ( match_text !== 'any' ) {
if ( this.good_match(f_obj,match_text) ) { // good match write a score to f_obj
q.inject(f_obj,orderby)
}
} else {
q.inject(f_obj,orderby)
}
}
}
// MATCHING....
// implement in descendant
score_match(check_txt,q_list,mult) {
let score = 1.0
return(score*mult)
}
good_match(f_obj,match_text,qry) {
return(true)
}
// END OF MATCHING....
// _run_query
_run_query(match_text,orderby,qry) {
let results = false
if ( match_text === 'any' ) {
// returns everything sorted according to the parameter or a default creation date
// these are pre-sorted
results = this.from_all_files(orderby)
//
} else if ( qry.op_is_special_id_key(orderby) ) {
// For exact matches. Pull from the pre-sorted by filtering
results = this.search_one_all_files(this.identifier_key,match_text)
//
} else if ( qry.op_is_special_key(orderby) ) {
// query can indicate the field to match on
let field = qry.special_key(orderby)
results = this.search_by_field_all_files(field,match_text)
//
} else if ( qry.op_is_function(orderby) ) {
let func = qry.op_get_function(match_text)
if ( func ) {
results = this.search_by_function_all_files(funcdef)
}
} else if ( qry.op_is_stored_function(orderby) ) { // fuctions loaded at startup.
let func = this._search_functions[match_text]
if ( func ) {
results = this.search_by_function_all_files(funcdef)
}
} else {
//
try {
//
let self = this
results = this.global_file_list.filter((object) => {
return self.good_match(object,match_text,qry)
})
//
switch ( orderby ) {
case 'update_date' : {
results = this.sort_by_updated(results)
break;
}
case 'score' : {
results = this.sort_by_score(results)
break;
}
case 'create_date' :
default: {
results = this.sort_by_created(results)
break;
}
}
} catch(e) {
console.log(e)
return([]) // something went wrong... so nothing
}
}
// // results is an array object (basic javascript)
if ( results.length ) {
// don't create copies... keep a index for the viewer.. but ref the object fix it on delivery
results = results.map((item,index)=> {
let c_item = {
"entry" : index + 1,
"score" : item.score,
"item_ref" : item
}
return(c_item)
})
}
//
if ( qry.is_aggregation() ) {
let aggregate_results = this.aggregate(results,qry)
return aggregate_results
}
//
return results // the list of matching items
}
/**
*
* @param {*} data
* @param {*} qry
* @returns
*/
aggregate(data,qry) {
let func = false
if ( qry.aggregate_is_code() ) {
func = qry.op_get_function(match_text)
} else {
let funcname = qry.aggregate_function_id()
func = this._search_functions[funcname]
if ( func === undefined ) {
return data
}
}
let initial_value = qry.initial_aggregate()
let reduction = data.reduce(func,initial_value)
return {
'data' : data,
'aggr' : reduction
}
}
///
/**
* prune
*
* @param {number} delta_timeout
* @returns number - the number of items pruned.
*
*/
prune(delta_timeout) {
//
let prune_time = Date.now()
//
let searches = Object.keys(this.local_active_searches)
//
let count = 0
searches.forEach(srch => {
let q_obj = this.local_active_searches[srch]
let when = q_obj.when
if ( (prune_time - delta_timeout) > when ) {
let q = this.local_active_searches[srch]
q.clear()
delete this.local_active_searches[srch]
count++
}
})
//
return count
}
/// FILES
// ----
/**
* backup_searches
* @param {boolean} do_halt -- indicates if the program is shutting down
*/
async backup_searches(do_halt) {
console.log("backing up searches")
//
let backup_serials = {}
for ( let qry in this.local_active_searches ) {
let QR = this.local_active_searches[qry]
backup_serials[qry] = QR.serialize()
}
//
let status = await this.fos.output_json(this.backup_file,backup_serials)
if ( status === false ) {
if ( do_halt === true ) {
console.log("halting ... done backing up searches")
console.log("exiting")
process.exit(0)
}
}
}
/**
* restore_searches
*
* load the searches that were previously save to the configured backup file
*/
async restore_searches() {
console.log("restoring searches")
try {
//
this.local_active_searches = {}
let stored_obj = await this.fos.load_json_data_at_path(this.backup_file)
//
for ( let k in stored_obj ) {
let restored = stored_obj[k]
let q = new this.QInterfaceClass('',this.functional_evaluator,restored)
q.deserialize(this.get_global_tracking_map())
this.local_active_searches[k] = q
}
//
} catch (e) {
console.log(e)
}
}
/**
* clear
*
* Wipes out a query
*
* @param {string} query
*/
async clear(query) {
let qhash = await this.query_hasher(query)
let q = this.local_active_searches[qhash]
if ( q ) {
q.clear()
delete this.local_active_searches[qhash]
}
}
// ---- ---- ---- ---- ---- ---- ---- ---- ----
/**
* app_specific_file_removal
*
* Note: tracking and `_id` differ in that tracking may be used across a number of search applications.
* `_id` may be a user id an will key into individual searches in a search instance.
*
* @param {string} tracking -- the tracking field string, should be attached to the objet
* @param {object} stored - an object that has been found in the global file list and is being removed
*/
app_specific_file_removal(tracking,stored) {
//
let statics = this.QInterfaceClass
let individual_search = statics.individual_search_key(tracking,this.identifier_key)
let q = this.local_active_searches[individual_search]
if ( q !== undefined ) {
delete this.local_active_searches[individual_search]
}
if ( stored._id !== undefined ) {{
let id_search = statics.id_search_key(stored._id)
q = this.local_active_searches[id_search]
if ( q !== undefined ) {
delete this.local_active_searches[id_search]
}
}}
//
}
//
}
module.exports.Searching = Searching