Skip to content

Commit

Permalink
New version supporting explicit filtering of versions
Browse files Browse the repository at this point in the history
  • Loading branch information
Adrian Kosmaczewski committed Jul 17, 2023
1 parent 1fdce80 commit 97bf6fe
Show file tree
Hide file tree
Showing 9 changed files with 2,024 additions and 200 deletions.
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ ENV PLATFORM alpine
ENV ARCH x64
RUN /usr/local/bin/pkg-fetch ${NODE} ${PLATFORM} ${ARCH}

# Build the application
# Run tests
WORKDIR /app
COPY . /app
RUN npm install
RUN npm test

# Build the application
RUN npm run build

# Package the result into a binary without dependencies
Expand Down
2 changes: 1 addition & 1 deletion index/files.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion index/lunr.json

Large diffs are not rendered by default.

1,969 changes: 1,889 additions & 80 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
{
"name": "search-engine",
"version": "1.8.0",
"version": "1.9.0",
"description": "An embedded search engine in Express to use in multi-container pods",
"main": "index.js",
"scripts": {
"dev": "node_modules/.bin/ts-node src/index.ts",
"test": "npx ts-mocha --timeout 10000 spec/*",
"build": "tsc -p .",
"start": "node dist/index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@types/express": "^4.17.17",
"express": "^4.17.1",
"log-timestamp": "^0.3.0",
"lunr": "^2.3.8"
"lunr": "^2.3.9",
"ts-mocha": "^10.0.0"
},
"devDependencies": {
"@types/express": "^4.17.2",
"@types/chai": "^4.3.5",
"@types/chai-as-promised": "^7.1.5",
"@types/lunr": "^2.3.2",
"@types/mocha": "^10.0.1",
"@types/node": "^12.12.17",
"chai": "^4.3.7",
"mocha": "^10.2.0",
"ts-node": "^10.0.0",
"typescript": "^5.0.0"
}
Expand Down
42 changes: 42 additions & 0 deletions spec/engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect } from 'chai'
import { FileRepository, getConfigSync, search } from '../src/engine'
import lunr from 'lunr'

let files: any
let lunrIndex: any

describe ('Search engine', () => {
before(() => {
files = getConfigSync('files.json', 'index') as FileRepository
const lunrIndexSource = getConfigSync('lunr.json', 'index')
lunrIndex = lunr.Index.load(lunrIndexSource)
})

it('returns results', () => {
const results = search(lunrIndex, files, 'backup')
expect(results).not.to.be.null
})

it('by default returns a maximum of 50 results', () => {
const results = search(lunrIndex, files, 'backup')
expect(results.length).to.equal(50)
})

it('the "main" version works just like "master"', () => {
const results1 = search(lunrIndex, files, 'backup', undefined, 'master')
const results2 = search(lunrIndex, files, 'backup', undefined, 'main')
expect(results1.length).to.equal(50)
expect(results2.length).to.equal(50)
expect(results1).to.deep.equal(results2)
})

it('can be filtered by version', () => {
const results = search(lunrIndex, files, 'backup', undefined, '0.1')
expect(results.length).to.equal(7)
})

it('can be return a maximum number of results', () => {
const results = search(lunrIndex, files, 'backup', 2, '0.1')
expect(results.length).to.equal(2)
})
})
63 changes: 63 additions & 0 deletions src/engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import SearchResult from "./search_result"
import path from 'path'
import fs from 'fs'

export type FileRepository = { [ref: string]: SearchResult }

/**
* Performs the actual search against the Lunr index.
* This function takes a "version" parameter; if this parameter is "master" or "main", then all search
* results are included in the response. If a different value is provided, only those values are returned.
* @param lunrIndex The Lunr.js index
* @param files The repository of files
* @param query Text to search
* @param count (Optional, default 50) Number of items to return
* @param version (Optional, default "master") Version of the search results to return
*/
export function search(lunrIndex: lunr.Index, files: FileRepository, query: string, count = 50, version = 'master'): SearchResult[] {
if (isEmptyOrBlank(query)) {
return []
}
const results: SearchResult[] = lunrIndex.search(query.trim())
.map((result: lunr.Index.Result) => {
return files[result.ref]
})
.filter((result: SearchResult) => {
// This "or" statement short-circuits the evaluation: if "master" or "main" are mentioned,
// the result is included; otherwise, if the version coincides, it is included.
if (version === 'master' || version === 'main' || result.version === version) {
return result
}
})
.slice(0, count)
return results
}

/**
* Returns a JSON object with the contents of a configuration file.
* @param file A valid filename
*/
export function getConfigSync(file: string, folder = '/site/index'): object {
const filepath = path.join(folder, file)
return readJsonFileSync(filepath)
}

/**
* Returns true if a string is null, undefined, empty or blank after trimming.
* @param s The string to test
*/
function isEmptyOrBlank(s: string): boolean {
return (!s || s.length === 0 || !s.trim())
}

/**
* Returns the contents of the JSON file whose name is passed as parameter.
* @param file A valid filename
*/
function readJsonFileSync(filepath: string, encoding = 'utf8'): object {
if (!fs.existsSync(filepath)) {
throw `File "${filepath}" does not exist'`
}
const file = fs.readFileSync(filepath, encoding)
return JSON.parse(file)
}
61 changes: 6 additions & 55 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,10 @@
import SearchResult from "./search_result"
import { Request } from "express"
import { Response } from "express-serve-static-core"

import express from 'express'
import path from 'path'
import fs from 'fs'
import lunr from 'lunr'
import 'log-timestamp'

type FileRepository = { [ref: string]: SearchResult }

/**
* Returns the contents of the JSON file whose name is passed as parameter.
* @param file A valid filename
*/
function readJsonFileSync(filepath: string, encoding = 'utf8'): object {
if (!fs.existsSync(filepath)) {
throw `File "${filepath}" does not exist'`
}
const file = fs.readFileSync(filepath, encoding)
return JSON.parse(file)
}

/**
* Returns a JSON object with the contents of a configuration file.
* @param file A valid filename
*/
function getConfigSync(file: string): object {
const filepath = path.join('/site/index', file)
return readJsonFileSync(filepath)
}

/**
* Returns true if a string is null, undefined, empty or blank after trimming.
* @param s The string to test
*/
function isEmptyOrBlank(s: string): boolean {
return (!s || s.length === 0 || !s.trim())
}

/**
* Performs the actual search against the Lunr index.
* @param lunrIndex The Lunr.js index
* @param files The repository of files
* @param query Text to search
* @param count (Optional, default 10) Number of items to return
*/
function search(lunrIndex: lunr.Index, files: FileRepository, query: string, count = 10): SearchResult[] {
console.log(`Searching for "${query}" (count = ${count})`)
if (isEmptyOrBlank(query)) {
return []
}
const results: SearchResult[] = lunrIndex.search(query.trim())
.slice(0, count)
.map((result: lunr.Index.Result) => {
return files[result.ref]
})
return results
}
import { FileRepository, getConfigSync, search } from "./engine"

// Entry point of the application
try {
Expand All @@ -72,7 +19,11 @@ try {

// API endpoint
app.get('/search', (req: Request, res: Response) => {
res.send(search(lunrIndex, files, req.query.q, req.query.c))
const query = req.query['q'] as string
const version = req.query["v"] as string
const countString = req.query['c'] as string
const count: number = (countString) ? parseInt(countString) : 50
res.send(search(lunrIndex, files, query, count, version))
})

app.listen(port, () => console.log(`Search engine listening on port ${port}`))
Expand Down
67 changes: 8 additions & 59 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,63 +1,12 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */

/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */

/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */

/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"target": "es5",
"module": "commonjs",
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"strictNullChecks": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}

0 comments on commit 97bf6fe

Please sign in to comment.