import { Client } from '@elastic/elasticsearch' ; import { Persist } from '../../types/persist' ; import { ModuleCode , ModuleInformation } from '../../types/modules' ; import config from '../../config' ; import logger from '../logger' ; type ModuleSearchBody = { hits : { total : number ; hits : { _id : string ; _score : number ; _source : { moduleCode : ModuleCode ; } ; } [ ] ; } ; } ; const INDEX_NAME = 'modules_v2' ; const first_digit_tokenizer = { type : 'simple_pattern' , pattern : '[1-9]{1}' , } ; const first_token_limit_filter = { type : 'limit' , max_token_count : 1 , } ; const thousandizer_filter = { type : 'pattern_replace' , pattern : '(\\d+)' , replacement : '$1000' , } ; async function createIndex ( client ) { try { await client . indices . create ( { index : INDEX_NAME , body : { settings : { analysis : { analyzer : { level_analyzer : { type : 'custom' , tokenizer : 'first_digit_tokenizer' , filter : [ 'first_token_limit_filter' , 'thousandizer_filter' ] , } , } , tokenizer : { first_digit_tokenizer } , filter : { first_token_limit_filter , thousandizer_filter } , } , index : { max_result_window : 20000 , } , } , } , } ) ; } catch ( e ) { if ( e . name !== 'ResponseError' || e . meta . body . error . type !== 'resource_already_exists_exception' ) { throw e ; } } await client . indices . putMapping ( { index : INDEX_NAME , body : { properties : { workload : { type : 'text' } , moduleCredit : { type : 'short' } , moduleCode : { type : 'text' , fields : { keyword : { type : 'keyword' , ignore_above : 10 , } , level : { type : 'text' , analyzer : 'level_analyzer' , fielddata : true , } , } , } , semesterData : { type : 'nested' , } , } , } , } ) ; return client ; } export default class ElasticPersist implements Persist { private readonly client : Promise < Client > ; constructor ( ) { if ( ! config . elasticConfig ) { throw new Error ( 'elasticConfig in config.json is not set' ) ; } const client = new Client ( config . elasticConfig ) ; this . client = createIndex ( client ) ; } deleteModule = async ( moduleCode ) => { const client = await this . client ; await client . delete ( { id : moduleCode , index : INDEX_NAME , } ) ; } ; moduleInfo = async ( moduleInfo ) => { const bulkBody = [ ] ; for ( const module of moduleInfo ) { bulkBody . push ( { index : { _id : module . moduleCode } , } ) ; if ( module . attributes ) { bulkBody . push ( { ... module , moduleAttributes : Object . keys ( module . attributes ) , } ) ; } else { bulkBody . push ( module ) ; } } const client = await this . client ; const res = await client . bulk ( { index : INDEX_NAME , body : bulkBody , } ) ; const { items } = res . body ; const erroredItems = items . filter ( ( i ) => { const { status } = i . index ; return status < 200 || status >= 300 ; } ) ; if ( erroredItems . length ) { logger . error ( `Insertion errors encountered` , { erroredLength : erroredItems . length , totalLength : res . body . items . length , } ) ; for ( const item of erroredItems ) { logger . error ( 'Error importing item' , item . index . error ) ; } } } ; facultyDepartments ( ) { return Promise . resolve ( ) ; } async getModuleCodes ( ) { const client = await this . client ; const { body } = await client . search ( { index : INDEX_NAME , body : { query : { match_all : { } , } , _source : 'moduleCode' , size : 20000 , } , } ) ; return ( body as ModuleSearchBody ) . hits . hits . map ( ( hit ) => hit . _source . moduleCode ) ; } module ( ) { return Promise . resolve ( ) ; } moduleAliases ( ) { return Promise . resolve ( ) ; } moduleInformation ( ) { return Promise . resolve ( ) ; } moduleList ( ) { return Promise . resolve ( ) ; } mpeModules ( ) { return Promise . resolve ( ) ; } semesterData ( ) { return Promise . resolve ( ) ; } timetable ( ) { return Promise . resolve ( ) ; } venueInformation ( ) { return Promise . resolve ( ) ; } venueList ( ) { return Promise . resolve ( ) ; } }