diff --git a/.DS_Store b/.DS_Store index 872a8a2..1bfd149 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..e24bac4 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,47 @@ +engines: + eslint: + enabled: true + duplication: + enabled: true + config: + languages: + - javascript + csslint: + enabled: false + # ... CONFIG CONTENT ... + checks: + comma-dangle: + enabled: false + import/no-unresolved: + enabled: false + no-console: + enabled: false + strict: + enabled: false + no-unused-vars: + enabled: false + no-undef: + enabled: false + class-methods-use-this: + enabled: false + no-param-reassign: + enabled: false + array-callback-return: + enabled: false + fixme: + enabled: true +ratings: + paths: + - "src/InvertedIndex.js" +exclude_paths: +- "samples/*" +- "*.json" +- "src/app.js" +- "src/module.js" +- "css/*" +- "index.html" +- "specs/*" +- .travis.yml +- .eslintrc.json +- "spec/*" +- "images/*" \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index eb1816b..3d95cba 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ -css/ +src/public/css/ samples/ node_modules/ images/ @@ -11,4 +11,5 @@ spec/*.html *.html karma.conf.js Profile +build/ diff --git a/eslintrc.json b/.eslintrc.json similarity index 92% rename from eslintrc.json rename to .eslintrc.json index bb92245..fdab344 100644 --- a/eslintrc.json +++ b/.eslintrc.json @@ -11,15 +11,14 @@ }, "rules": { "import/no-unresolved": 0, + "max-len":[2, 80, 2], "one-var": 0, "one-var-declaration-per-line": 0, "new-cap": 0, "no-undef": 0, "func-names":0, "consistent-return": 0, - "class-methods-use-this": 0, "no-param-reassign": 0, - "no-prototype-builtins": 0, "comma-dangle": 0, "curly": [ 1, "multi-line" ], "no-shadow": [ 1, { "allow": [ "req", "res", "err" ] } ], diff --git a/.gitignore b/.gitignore index d8cca13..2f9bd06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ .DS_Store node_modules/ -test.js -testclass.js coverage/ \ No newline at end of file diff --git a/Profile b/Procfile similarity index 100% rename from Profile rename to Procfile diff --git a/README.md b/README.md index ce4bfac..8836381 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Coverage Status](https://coveralls.io/repos/github/andela-aonifade/dplex/badge.svg?branch=development)](https://coveralls.io/github/andela-aonifade/dplex?branch=development) +[![Coverage Status](https://s3.amazonaws.com/assets.coveralls.io/badges/coveralls_98.svg?branch=development)](https://coveralls.io/github/andela-aonifade/dplex?branch=development) [![Code Climate](https://codeclimate.com/github/andela-aonifade/dplex/badges/gpa.svg)](https://codeclimate.com/github/andela-aonifade/dplex) [![Build Status](https://travis-ci.org/andela-aonifade/dplex.svg?branch=development)](https://travis-ci.org/andela-aonifade/dplex) @@ -40,5 +40,6 @@ It can also be used locally by following the steps below - The application can not be distinguished between plural and singular words. It also does not distinguish between the past tense form of a verb. It does not identify synonyms and sees numbers as string ## More information +- [DPlex Wiki](https://github.com/andela-aonifade/dplex/wiki) - [Inverted Index - Wikipedia](https://en.wikipedia.org/wiki/Inverted_index) - [Inverted Index](https://www.elastic.co/guide/en/elasticsearch/guide/current/inverted-index.html) diff --git a/build/bundle.js b/build/bundle.js new file mode 100644 index 0000000..1bb4c08 --- /dev/null +++ b/build/bundle.js @@ -0,0 +1,303 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { + beforeAll(() => { + const indexInstance = new InvertedIndex(); + const validBook = [{ title: 'Welcome to Test Environment', + text: 'Enjoy this file' }]; + const books = [ + { + title: 'Alice in Wonderland', + text: + 'Alice falls into a rabbit hole and enters a world full of imagination.' + }, + + { + title: 'The Lord of the Rings: The Fellowship of the Ring.', + text: `An unusual alliance of man, elf, dwarf, + wizard and hobbit seek to destroy a powerful ring.` + }, + { + title: 'The Lord of the Rings: The Fellowship of the Ring.', + text: `An unusual alliance of man, elf, dwarf, + wizard and hobbit seek to destroy a powerful ring.` + } + ]; + const anotherBook = [{ + title: 'Alice the Great', + text: + 'There is no better way to greatness than not giving up' + }, + + { + title: 'Are you there for Development', + text: `I have tried so many times but it's been unyielding + but I have made up my mind to develop no matter the obstacle` + }]; + indexInstance.createIndex(books, 'books'); + indexInstance.createIndex(anotherBook, 'anotherBook'); + }); + describe('InvertedIndex class', () => { + it('should check that the class has a createIndex method', () => { + expect(typeof indexInstance.createIndex).toBe('function'); + }); + + it('should check that the class has a readFile method', () => { + expect(typeof InvertedIndex.readFile).toBe('function'); + }); + + it('should check that the class has a validateFile method', () => { + expect(typeof InvertedIndex.validateFile).toBe('function'); + }); + + it('should check that the class has a tokenize method', () => { + expect(typeof InvertedIndex.tokenize).toBe('function'); + }); + + it('should check that the class has a getDocumentTokens method', () => { + expect(typeof InvertedIndex.getDocumentTokens).toBe('function'); + }); + + it('should check that the class has a getIndex method', () => { + expect(typeof indexInstance.getIndex).toBe('function'); + }); + + it('should check that the class has a searchIndex method', () => { + expect(typeof indexInstance.searchIndex).toBe('function'); + }); + + it('should check that the class has a getSearchResults method', () => { + expect(typeof indexInstance.getSearchResults).toBe('function'); + }); + + it('should check that the class has a getDocuments method', () => { + expect(typeof indexInstance.getDocuments).toBe('function'); + }); + }); + + describe('Read File', () => { + it('should return false for an invalid filename extension', () => { + const badFile = { name: 'badfileextension.jpg' }; + const anotherBadFile = { name: 'badfileextension.jsona' }; + InvertedIndex.readFile(badFile).then((response) => { + expect(response).toBeFalsy(); + }); + InvertedIndex.readFile(anotherBadFile).then((response) => { + expect(response).toBeFalsy(); + }); + }); + const bookFile = new File([JSON.stringify(validBookFile)], + 'books.json', { type: 'application/json' }); + it('should return appropriate value for a valid json file', () => { + InvertedIndex.readFile(bookFile).then((response) => { + expect(response[0].title).toEqual(books[0].title); + }); + }); + }); + + describe('Create Index', () => { + it('should return mapped indices to words in a JSON file', () => { + const expectedResult = + { alice: [0], + falls: [0], + into: [0], + a: [0, 1, 2], + rabbit: [0], + hole: [0], + and: [0, 1, 2], + enters: [0], + world: [0], + full: [0], + of: [0, 1, 2], + imagination: [0], + in: [0], + wonderland: [0], + an: [1, 2], + unusual: [1, 2], + alliance: [1, 2], + man: [1, 2], + elf: [1, 2], + dwarf: [1, 2], + wizard: [1, 2], + hobbit: [1, 2], + seek: [1, 2], + to: [1, 2], + destroy: [1, 2], + powerful: [1, 2], + ring: [1, 2], + the: [1, 2], + lord: [1, 2], + rings: [1, 2], + fellowship: [1, 2] }; + expect(indexInstance.filesIndexed.books.index) + .toEqual(expectedResult); + }); + it('should return false for file with no content', () => { + const term = {}; + expect(indexInstance.createIndex(term, 'term')).toBeFalsy(); + }); + }); + describe('Search Index', () => { + it('should search through single files that are indexed', () => { + const requiredOutput = { alice: [0], + and: [0, 1, 2], + unusual: [1, 2], + imagination: [0] }; + const searchTerm = indexInstance + .searchIndex('Alice, and her unusual imagination', 'books'); + expect(Object.keys(searchTerm[0].indexes)) + .toEqual(Object.keys(requiredOutput)); + expect(searchTerm[0].indexes).toEqual(requiredOutput); + }); + it('should return false for an empty String', () => { + const term = ''; + expect(indexInstance.searchIndex(term, 'books')) + .toBeFalsy(); + }); + it('should return an empty object for words not found', () => { + const term = 'Aeroplane'; + const expectedOutput = indexInstance.searchIndex(term, 'books'); + expect(expectedOutput[0].indexes).toEqual({ }); + }); + it('should return appropriate result for when all files is selected', + () => { + const expectedOutput = + [{ indexes: { alice: [0], the: [1, 2] }, + searchedFile: 'books', + documents: [0, 1, 2] }, + { indexes: { alice: [0], + is: [0], + the: [0, 1] }, + searchedFile: 'anotherBook', + documents: [0, 1] }, + { indexes: { }, + searchedFile: 'term', + documents: [] }]; + expect(indexInstance + .searchIndex('Alice is the', 'all')).toEqual(expectedOutput); + }); + }); + + describe('Tokenize words', () => { + it('should strip out special characters from text in documents', () => { + let excerpt = 'Alice l##$oves her ima&&gination?'; + const expectedTokens = ['alice', 'loves', 'her', 'imagination']; + excerpt = InvertedIndex.tokenize(excerpt); + expect(excerpt).toEqual(expectedTokens); + }); + }); + + describe('Get index', () => { + it('should return the appropriate output for the given filename', () => { + const filename = 'books'; + const expectedOutput = { alice: [0], + falls: [0], + into: [0], + a: [0, 1, 2], + rabbit: [0], + hole: [0], + and: [0, 1, 2], + enters: [0], + world: [0], + full: [0], + of: [0, 1, 2], + imagination: [0], + in: [0], + wonderland: [0], + an: [1, 2], + unusual: [1, 2], + alliance: [1, 2], + man: [1, 2], + elf: [1, 2], + dwarf: [1, 2], + wizard: [1, 2], + hobbit: [1, 2], + seek: [1, 2], + to: [1, 2], + destroy: [1, 2], + powerful: [1, 2], + ring: [1, 2], + the: [1, 2], + lord: [1, 2], + rings: [1, 2], + fellowship: [1, 2] }; + expect(indexInstance.getIndex(filename)) + .toEqual(expectedOutput); + }); + it('should return false for an empty filename', () => { + const filename = ''; + expect(indexInstance.getIndex(filename)) + .toBeFalsy(); + }); + }); + + describe('Validate File', () => { + it('should return false for incorrect document structure', () => { + const term = [{ t1: 'Welcome home', text: 'This is really home' }]; + expect(InvertedIndex.validateFile(term, 'term')).toBeFalsy(); + }); + it('should return true for correct document structure', () => { + const term = [{ title: 'Welcome home', text: 'This is really home' }]; + expect(InvertedIndex.validateFile(term, 'term')).toBeTruthy(); + }); + }); + + describe('Get Document Tokens', () => { + it('should return the appropriate object for a given document', + () => { + const expectedOutput = { documentCount: 0, + textTokens: ['welcome', 'this', 'is', 'a', 'test', 'document'] }; + const documentCount = 0; + const term = [{ text: 'Welcome', title: 'This is a test document' }]; + expect(InvertedIndex + .getDocumentTokens(term, documentCount)).toEqual(expectedOutput); + }); + }); + + describe('Get Document Data', () => { + it('should return the appropriate array of documents for a given file', + () => { + const expectedOutput = [0, 1, 2]; + expect(indexInstance + .getDocuments('books')).toEqual(expectedOutput); + }); + }); + + describe('Get Search result Data', () => { + it('should return the appropriate result for tokens searched', + () => { + const words = 'Alice is a girl'; + const expectedOutput = { alice: [0], a: [0, 1, 2] }; + expect(indexInstance + .getSearchResults(words, 'books')).toEqual(expectedOutput); + }); + }); + + describe('Get Construct Index Data', () => { + it('should return the appropriate indexed words for a given document', + () => { + const documentTokens = [{ documentCount: 0, + textTokens: ['welcome', 'this', 'is', 'a', 'test', 'document'] }]; + const expectedOutput = + { welcome: [0], this: [0], is: [0], a: [0], test: [0], document: [0] }; + expect(InvertedIndex.constructIndex(documentTokens)) + .toEqual(expectedOutput); + }); + }); +}); + +},{"../samples/books.json":1}]},{},[2]) \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 8bd41e1..5cd99d3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,18 +2,26 @@ const gulp = require('gulp'), connect = require('gulp-connect'), eslint = require('gulp-eslint'), + rename = require('gulp-rename'), coveralls = require('gulp-coveralls'), + browserify = require('gulp-browserify'), browserSync = require('browser-sync').create(); +gulp.task('scripts', () => { + gulp.src('./spec/InvertedIndex.spec.js') + .pipe(browserify()) + .pipe(rename('bundle.js')) + .pipe(gulp.dest('./build')); +}); gulp.task('watch', () => { - gulp.watch('src/*.js', ['reload']); - gulp.watch('css/*.css', ['reload']); + gulp.watch('./src/*.js', ['reload']); + gulp.watch('./src/public/css/*.css', ['reload']); gulp.watch('*.html', ['reload']); }); gulp.task('test', ['pre-test'], () => ( - gulp.src('test/spec/*.js') + gulp.src('spec/*.js') .on('end', () => { gulp.src('coverage/lcov.info') .pipe(coveralls()); @@ -29,18 +37,18 @@ gulp.task('serve', ['watch'], () => { }); gulp.task('lint', () => { - gulp.src(['src/invertedindex.js', '!node_modules/**']) + gulp.src(['src/InvertedIndex.js', '!node_modules/**']) .pipe(eslint()) .pipe(eslint.format()) .pipe(eslint.failAfterError()); }); gulp.task('test-watch', () => { - gulp.watch('jasmine/spec/*.js', ['test-reload']); + gulp.watch('spec/*.js', ['test-reload']); }); gulp.task('test-reload', () => { - gulp.src(['jasmine/spec/*.js']).pipe(connect.reload()); + gulp.src(['spec/*.js']).pipe(connect.reload()); }); gulp.task('connect', () => { @@ -55,12 +63,13 @@ gulp.task('reload', () => { gulp.src( ['*.html', 'src/*.js', - 'css/*.css' + '*.css' ]).pipe(connect.reload()); }); gulp.task('default', [ + 'scripts', 'reload', 'test-watch', 'test-reload', diff --git a/images/empty_text_search.png b/images/empty_text_search.png new file mode 100644 index 0000000..5013d28 Binary files /dev/null and b/images/empty_text_search.png differ diff --git a/images/search_screenshot.png b/images/search_screenshot.png new file mode 100644 index 0000000..0ee77b0 Binary files /dev/null and b/images/search_screenshot.png differ diff --git a/karma.conf.js b/karma.conf.js index 21f6a3e..0927e05 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,12 +9,18 @@ module.exports = (config) => { // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine'], + frameworks: ['browserify', 'jasmine'], + plugins: [ + 'karma-browserify', + 'karma-jasmine', + 'karma-chrome-launcher', + 'karma-coverage' + ], // list of files / patterns to load in the browser files: [ - 'src/invertedindex.js', + 'src/InvertedIndex.js', 'spec/*.js' ], @@ -31,7 +37,8 @@ module.exports = (config) => { // source files, that you wanna generate coverage for // do not include tests or libraries // (these files will be instrumented by Istanbul) - 'src/invertedindex.js': ['coverage'] + 'src/InvertedIndex.js': ['coverage'], + 'spec/InvertedIndex.spec.js': ['browserify'] }, customLaunchers: { diff --git a/package.json b/package.json index 51ca564..7bb356d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "inverted-index", + "name": "DPlex", "version": "1.0.0", "description": "Dplex is an inverted index App to help in easy word search in a document", "scripts": { @@ -8,7 +8,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/andela-aonifade/invertedindex.git" + "url": "git+https://github.com/andela-aonifade/dplex.git" }, "keywords": [ "js", @@ -21,18 +21,20 @@ "author": "Anu Onifade", "license": "ISC", "bugs": { - "url": "https://github.com/andela-aonifade/invertedindex/issues" + "url": "https://github.com/andela-aonifade/dplex/issues" }, - "homepage": "https://github.com/andela-aonifade/invertedindex#readme", + "homepage": "https://github.com/andela-aonifade/dplex#readme", "dependencies": { "browser-sync": "^2.18.8", "connect": "^3.6.0", "coveralls": "^2.13.0", "eslint": "^3.19.0", + "express": "^4.15.2", "gulp": "^3.9.1", "gulp-connect": "^5.0.0", "gulp-coveralls": "^0.1.4", "gulp-eslint": "^3.0.1", + "karma-jasmine": "^1.1.0", "serve-static": "^1.12.1" }, "devDependencies": { @@ -43,11 +45,14 @@ "eslint-plugin-import": "^2.2.0", "eslint-plugin-jsx-a11y": "^4.0.0", "eslint-plugin-react": "^6.10.3", + "gulp-browserify": "^0.5.1", + "gulp-rename": "^1.2.2", "jasmine-core": "^2.5.2", "karma": "^1.5.0", + "karma-browserify": "^5.1.1", "karma-chrome-launcher": "^2.0.0", "karma-coverage": "^1.1.1", "karma-firefox-launcher": "^1.0.1", - "karma-jasmine": "^1.0.2" + "karma-jasmine": "^1.1.0" } } diff --git a/samples/anotherBook.json b/samples/anotherBook.json new file mode 100644 index 0000000..c4c6f74 --- /dev/null +++ b/samples/anotherBook.json @@ -0,0 +1,10 @@ +[ + { + "title": "Alice the Great", + "text": "There is no better way to greatness than not giving up" + }, + { + "title": "Are you there for Development", + "text": "I have tried so many times but it's been unyielding but I have made up my mind to develop no matter the obstacle" + } +] \ No newline at end of file diff --git a/samples/books.json b/samples/books.json index a90605e..5cf344c 100644 --- a/samples/books.json +++ b/samples/books.json @@ -3,7 +3,6 @@ "title": "Alice in Wonderland", "text": "Alice falls into a rabbit hole and enters a world full of imagination." }, - { "title": "The Lord of the Rings: The Fellowship of the Ring.", "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." diff --git a/samples/largebooks.json b/samples/largebooks.json new file mode 100644 index 0000000..9e4205e --- /dev/null +++ b/samples/largebooks.json @@ -0,0 +1,80 @@ +[ + { + "title": "Alice in Wonderland", + "text": "Alice falls into a rabbit hole and enters a world full of imagination." + }, + + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + }, + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + }, + { + "title": "Alice in Wonderland", + "text": "Alice falls into a rabbit hole and enters a world full of imagination." + }, + + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + }, + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + }, + { + "title": "Alice in Wonderland", + "text": "Alice falls into a rabbit hole and enters a world full of imagination." + }, + + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + }, + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + }, + { + "title": "Alice in Wonderland", + "text": "Alice falls into a rabbit hole and enters a world full of imagination." + }, + + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + }, + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + }, + { + "title": "Alice in Wonderland", + "text": "Alice falls into a rabbit hole and enters a world full of imagination." + }, + + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + }, + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + }, + { + "title": "Alice in Wonderland", + "text": "Alice falls into a rabbit hole and enters a world full of imagination." + }, + + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + }, + { + "title": "The Lord of the Rings: The Fellowship of the Ring.", + "text": "An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring." + } +] diff --git a/server.js b/server.js index b789121..2e4e3a1 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,14 @@ -const connect = require('connect'); -const serveStatic = require('serve-static'); +const express = require('express'); +const path = require('path'); +const app = express(); + +// Routes +app.get('/', (req, res) => { + res.sendFile(`${__dirname}/src/index.html`); +}); +app.use(express.static(path.join(__dirname, 'src'))); + +// Start server const port = process.env.PORT || 3000; -connect().use(serveStatic('./src')).listen(port); +app.listen(port, () => console.log(`Listening on port ${port}`)); diff --git a/spec/InvertedIndex.spec.js b/spec/InvertedIndex.spec.js new file mode 100644 index 0000000..a09c0f0 --- /dev/null +++ b/spec/InvertedIndex.spec.js @@ -0,0 +1,258 @@ +const books = require('../samples/books.json'); +const anotherBook = require('../samples/anotherBook.json'); + +const indexInstance = new InvertedIndex(); + +describe('InvertedIndex class', () => { + beforeAll(() => { + indexInstance.createIndex(books, 'books'); + indexInstance.createIndex(anotherBook, 'anotherBook'); + }); + describe('InvertedIndex class', () => { + it('should check that the class has a createIndex method', () => { + expect(typeof indexInstance.createIndex).toBe('function'); + }); + + it('should check that the class has a readFile method', () => { + expect(typeof InvertedIndex.readFile).toBe('function'); + }); + + it('should check that the class has a validateFile method', () => { + expect(typeof InvertedIndex.validateFile).toBe('function'); + }); + + it('should check that the class has a tokenize method', () => { + expect(typeof InvertedIndex.tokenize).toBe('function'); + }); + + it('should check that the class has a getDocumentTokens method', () => { + expect(typeof InvertedIndex.getDocumentTokens).toBe('function'); + }); + + it('should check that the class has a getIndex method', () => { + expect(typeof indexInstance.getIndex).toBe('function'); + }); + + it('should check that the class has a searchIndex method', () => { + expect(typeof indexInstance.searchIndex).toBe('function'); + }); + + it('should check that the class has a getSearchResults method', () => { + expect(typeof indexInstance.getSearchResults).toBe('function'); + }); + + it('should check that the class has a getDocuments method', () => { + expect(typeof indexInstance.getDocuments).toBe('function'); + }); + }); + + describe('Read File', () => { + it('should return false for an invalid filename extension', () => { + const badFile = { name: 'badfileextension.jpg' }; + const anotherBadFile = { name: 'badfileextension.jsona' }; + InvertedIndex.readFile(badFile).then((response) => { + expect(response).toBeFalsy(); + }); + InvertedIndex.readFile(anotherBadFile).then((response) => { + expect(response).toBeFalsy(); + }); + }); + const bookFile = new File([JSON.stringify(books)], + 'books.json', { type: 'application/json' }); + it('should return appropriate value for a valid json file', () => { + InvertedIndex.readFile(bookFile).then((response) => { + expect(response[0].title).toEqual(books[0].title); + }); + }); + }); + + describe('Create Index', () => { + it('should return mapped indices to words in a JSON file', () => { + const expectedResult = + { alice: [0], + falls: [0], + into: [0], + a: [0, 1, 2], + rabbit: [0], + hole: [0], + and: [0, 1, 2], + enters: [0], + world: [0], + full: [0], + of: [0, 1, 2], + imagination: [0], + in: [0], + wonderland: [0], + an: [1, 2], + unusual: [1, 2], + alliance: [1, 2], + man: [1, 2], + elf: [1, 2], + dwarf: [1, 2], + wizard: [1, 2], + hobbit: [1, 2], + seek: [1, 2], + to: [1, 2], + destroy: [1, 2], + powerful: [1, 2], + ring: [1, 2], + the: [1, 2], + lord: [1, 2], + rings: [1, 2], + fellowship: [1, 2] }; + expect(indexInstance.filesIndexed.books.index) + .toEqual(expectedResult); + }); + it('should return false for file with no content', () => { + const term = {}; + expect(indexInstance.createIndex(term, 'term')).toBeFalsy(); + }); + }); + describe('Search Index', () => { + it('should search through single files that are indexed', () => { + const requiredOutput = { alice: [0], + and: [0, 1, 2], + unusual: [1, 2], + imagination: [0] }; + const searchTerm = indexInstance + .searchIndex('Alice, and her unusual imagination', 'books'); + expect(Object.keys(searchTerm[0].indexes)) + .toEqual(Object.keys(requiredOutput)); + expect(searchTerm[0].indexes).toEqual(requiredOutput); + }); + it('should return false for an empty String', () => { + const term = ''; + expect(indexInstance.searchIndex(term, 'books')) + .toBeFalsy(); + }); + it('should return an empty object for words not found', () => { + const term = 'Aeroplane'; + const expectedOutput = indexInstance.searchIndex(term, 'books'); + expect(expectedOutput[0].indexes).toEqual({ }); + }); + it('should return appropriate result for when all files is selected', + () => { + const expectedOutput = + [{ indexes: { alice: [0], the: [1, 2] }, + searchedFile: 'books', + documents: [0, 1, 2] }, + { indexes: { alice: [0], + is: [0], + the: [0, 1] }, + searchedFile: 'anotherBook', + documents: [0, 1] }, + { indexes: { }, + searchedFile: 'term', + documents: [] }]; + expect(indexInstance + .searchIndex('Alice is the', 'all')).toEqual(expectedOutput); + }); + }); + + describe('Tokenize words', () => { + it('should strip out special characters from text in documents', () => { + let excerpt = 'Alice l##$oves her ima&&gination?'; + const expectedTokens = ['alice', 'loves', 'her', 'imagination']; + excerpt = InvertedIndex.tokenize(excerpt); + expect(excerpt).toEqual(expectedTokens); + }); + }); + + describe('Get index', () => { + it('should return the appropriate output for the given filename', () => { + const filename = 'books'; + const expectedOutput = { alice: [0], + falls: [0], + into: [0], + a: [0, 1, 2], + rabbit: [0], + hole: [0], + and: [0, 1, 2], + enters: [0], + world: [0], + full: [0], + of: [0, 1, 2], + imagination: [0], + in: [0], + wonderland: [0], + an: [1, 2], + unusual: [1, 2], + alliance: [1, 2], + man: [1, 2], + elf: [1, 2], + dwarf: [1, 2], + wizard: [1, 2], + hobbit: [1, 2], + seek: [1, 2], + to: [1, 2], + destroy: [1, 2], + powerful: [1, 2], + ring: [1, 2], + the: [1, 2], + lord: [1, 2], + rings: [1, 2], + fellowship: [1, 2] }; + expect(indexInstance.getIndex(filename)) + .toEqual(expectedOutput); + }); + it('should return false for an empty filename', () => { + const filename = ''; + expect(indexInstance.getIndex(filename)) + .toBeFalsy(); + }); + }); + + describe('Validate File', () => { + it('should return false for incorrect document structure', () => { + const term = [{ t1: 'Welcome home', text: 'This is really home' }]; + expect(InvertedIndex.validateFile(term, 'term')).toBeFalsy(); + }); + it('should return true for correct document structure', () => { + const term = [{ title: 'Welcome home', text: 'This is really home' }]; + expect(InvertedIndex.validateFile(term, 'term')).toBeTruthy(); + }); + }); + + describe('Get Document Tokens', () => { + it('should return the appropriate object for a given document', + () => { + const expectedOutput = { documentCount: 0, + textTokens: ['welcome', 'this', 'is', 'a', 'test', 'document'] }; + const documentCount = 0; + const term = [{ text: 'Welcome', title: 'This is a test document' }]; + expect(InvertedIndex + .getDocumentTokens(term, documentCount)).toEqual(expectedOutput); + }); + }); + + describe('Get Document Method', () => { + it('should return the appropriate array of documents for a given file', + () => { + const expectedOutput = [0, 1, 2]; + expect(indexInstance + .getDocuments('books')).toEqual(expectedOutput); + }); + }); + + describe('Get Search result Method', () => { + it('should return the appropriate result for tokens searched', + () => { + const words = 'Alice is a girl'; + const expectedOutput = { alice: [0], a: [0, 1, 2] }; + expect(indexInstance + .getSearchResults(words, 'books')).toEqual(expectedOutput); + }); + }); + + describe('Get Construct Index Method', () => { + it('should return the appropriate indexed words for a given document', + () => { + const documentTokens = [{ documentCount: 0, + textTokens: ['welcome', 'this', 'is', 'a', 'test', 'document'] }]; + const expectedOutput = + { welcome: [0], this: [0], is: [0], a: [0], test: [0], document: [0] }; + expect(InvertedIndex.constructIndex(documentTokens)) + .toEqual(expectedOutput); + }); + }); +}); diff --git a/spec/SpecRunner.html b/spec/SpecRunner.html index 096d174..ac0ccf5 100644 --- a/spec/SpecRunner.html +++ b/spec/SpecRunner.html @@ -12,9 +12,9 @@ - + - + diff --git a/spec/dplex-test-spec.js b/spec/dplex-test-spec.js deleted file mode 100644 index 949edf9..0000000 --- a/spec/dplex-test-spec.js +++ /dev/null @@ -1,200 +0,0 @@ - -describe('InvertedIndex class', () => { - beforeEach(() => { - this.indexInstance = new InvertedIndex(); - this.validBook = [{ title: 'Welcome to Test Environment', - text: 'Enjoy this file' }]; - this.books = [ - { - title: 'Alice in Wonderland', - text: 'Alice falls into a rabbit hole and enters a world full of imagination.' - }, - - { - title: 'The Lord of the Rings: The Fellowship of the Ring.', - text: 'An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring.' - }, - { - title: 'The Lord of the Rings: The Fellowship of the Ring.', - text: 'An unusual alliance of man, elf, dwarf, wizard and hobbit seek to destroy a powerful ring.' - } - ]; - }); - describe('InvertedIndex class', () => { - it('should check that the class has a createIndex method', () => { - expect(typeof this.indexInstance.createIndex).toBe('function'); - }); - - it('should check that the class has a readFile method', () => { - expect(typeof this.indexInstance.readFile).toBe('function'); - }); - - it('should check that the class has a validateFile method', () => { - expect(typeof this.indexInstance.validateFile).toBe('function'); - }); - - it('should check that the class has a tokenize method', () => { - expect(typeof this.indexInstance.tokenize).toBe('function'); - }); - - it('should check that the class has a getDocumentTokens method', () => { - expect(typeof this.indexInstance.getDocumentTokens).toBe('function'); - }); - - it('should check that the class has a getIndex method', () => { - expect(typeof this.indexInstance.getIndex).toBe('function'); - }); - - it('should check that the class has a searchIndex method', () => { - expect(typeof this.indexInstance.searchIndex).toBe('function'); - }); - - it('should check that the class has a getSearchResults method', () => { - expect(typeof this.indexInstance.getSearchResults).toBe('function'); - }); - - it('should check that the class has a getDocuments method', () => { - expect(typeof this.indexInstance.getDocuments).toBe('function'); - }); - }); - - describe('Create Index', () => { - it('should return mapped indices to words in a JSON file', () => { - const expectedResult = - { alice: [0], - falls: [0], - into: [0], - a: [0, 1, 2], - rabbit: [0], - hole: [0], - and: [0, 1, 2], - enters: [0], - world: [0], - full: [0], - of: [0, 1, 2], - imagination: [0], - in: [0], - wonderland: [0], - an: [1, 2], - unusual: [1, 2], - alliance: [1, 2], - man: [1, 2], - elf: [1, 2], - dwarf: [1, 2], - wizard: [1, 2], - hobbit: [1, 2], - seek: [1, 2], - to: [1, 2], - destroy: [1, 2], - powerful: [1, 2], - ring: [1, 2], - the: [1, 2], - lord: [1, 2], - rings: [1, 2], - fellowship: [1, 2] }; - this.indexInstance.createIndex(this.books, 'books'); - expect(this.indexInstance.filesIndexed.books.index) - .toEqual(expectedResult); - }); - it('should return false for incorrect document structure', () => { - const term = { t1: 'Welcome home', text: 'This is really home' }; - expect(this.indexInstance.createIndex(term, 'term')).toBeFalsy(); - }); - }); - describe('Search Index', () => { - it('should search through single files that are indexed', () => { - const requiredOutput = { alice: [0], - and: [0, 1, 2], - unusual: [1, 2], - imagination: [0] }; - const searchTerm = this.indexInstance - .searchIndex('Alice, and her unusual imagination', 'books'); - expect(Object.keys(searchTerm[0].indexes)) - .toEqual(Object.keys(requiredOutput)); - expect(searchTerm[0].indexes).toEqual(requiredOutput); - }); - it('should return false for an empty String', () => { - const term = ''; - expect(this.indexInstance.searchIndex(term, 'books')) - .toBeFalsy(); - }); - }); - - describe('Tokenize words', () => { - it('should strip out special characters from excerpt in documents', () => { - let excerpt = 'Alice l##$oves her ima&&gination?'; - const expectedTokens = ['alice', 'loves', 'her', 'imagination']; - excerpt = this.indexInstance.tokenize(excerpt); - expect(excerpt).toEqual(expectedTokens); - }); - }); - - describe('Get index', () => { - it('should return false for an empty filename', () => { - const filename = 'books'; - const expectedOutput = { alice: [0], - falls: [0], - into: [0], - a: [0, 1, 2], - rabbit: [0], - hole: [0], - and: [0, 1, 2], - enters: [0], - world: [0], - full: [0], - of: [0, 1, 2], - imagination: [0], - in: [0], - wonderland: [0], - an: [1, 2], - unusual: [1, 2], - alliance: [1, 2], - man: [1, 2], - elf: [1, 2], - dwarf: [1, 2], - wizard: [1, 2], - hobbit: [1, 2], - seek: [1, 2], - to: [1, 2], - destroy: [1, 2], - powerful: [1, 2], - ring: [1, 2], - the: [1, 2], - lord: [1, 2], - rings: [1, 2], - fellowship: [1, 2] }; - this.indexInstance.createIndex(this.books, 'books'); - expect(this.indexInstance.getIndex(filename)) - .toEqual(expectedOutput); - }); - it('should return the appropriate output for the given filename', () => { - const filename = ''; - this.indexInstance.createIndex(this.books, 'books'); - expect(this.indexInstance.getIndex(filename)) - .toBeFalsy(); - }); - }); - - describe('Validate File', () => { - it('should return false for incorrect document structure', () => { - const term = { t1: 'Welcome home', text: 'This is really home' }; - expect(this.indexInstance.validateFile(term)).toBeFalsy(); - }); - it('should return true for correct document structure', () => { - const term = { title: 'Welcome home', text: 'This is really home' }; - expect(this.indexInstance.validateFile(term)).toBeTruthy(); - }); - }); - - describe('Get Document Data', () => { - it('should return the approriate object for a given document', - () => { - const expectedOutput = { documentNum: 0, - textTokens: ['welcome', 'this', 'is', 'a', 'test', 'document'] }; - const documentNum = 0; - const term = [{ text: 'Welcome', title: 'This is a test document' }]; - expect(this.indexInstance - .getDocumentTokens(term, documentNum)).toEqual(expectedOutput); - }); - }); -}); diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index 609f89e..00ce028 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -1,8 +1,8 @@ { "spec_dir": "spec", "spec_files": [ - "../src/invertedindex.js", - "dplex-test-spec.js" + "../src/InvertedIndex.js", + "InvertedIndex.spec.js" ], "helpers": [ "helpers/**/*.js" diff --git a/src/InvertedIndex.js b/src/InvertedIndex.js new file mode 100644 index 0000000..b22ce1b --- /dev/null +++ b/src/InvertedIndex.js @@ -0,0 +1,244 @@ +const hasProperty = Object.prototype.hasOwnProperty; +let instance = null; + +/** + * InvertedIndex class + * Contains methods for InvertedIndex + * Only allows single instance of the object to be created +*/ +class InvertedIndex { + /** + * constructor method ensures that there is only + * one instance of the class + * @return {object} - Instance of the class + */ + constructor() { + if (!instance) { + instance = this; + this.filesIndexed = {}; + this.fileContent = {}; + this.error = {}; + } + return instance; + } + + /** + * readFile read file from a given html element + * @param {object} fileContent - the json data to index + * @return {object|boolean} - When file have bad extension it returns + * false and return a json object if it is a good extension + */ + static readFile(fileContent) { + return new Promise((resolve, reject) => { + if (!fileContent.name.match(/\.json$/)) { + return reject(false); + } + const readFile = new FileReader(); + readFile.readAsText(fileContent); + readFile.onload = (file) => { + const content = file.target.result; + try { + resolve(JSON.parse(content)); + } catch (exception) { + reject(false); + } + }; + }); + } + + /** + * handleError handles error + * @param {string} fileName - Name of file being indexed or searched + * @param {string} errorMessage - Error message to be displayed + * @param {boolean} errorStatus - True or False + * @return {Object} error - Error Object + */ + handleError(fileName, errorMessage, errorStatus) { + delete this.filesIndexed[fileName]; + this.error.status = errorStatus; + this.error.message = errorMessage; + this.error.filename = fileName; + throw this.error; + } + + /** + * createIndex gets the json ready for indexing by tokenizing statements + * @param {object} fileContent - the json data to index + * @param {string} fileName - the name of the file to be indexed + * @return {boolean} - true or false if the createIndex was successful + */ + createIndex(fileContent, fileName) { + this.filesIndexed[fileName] = {}; + const words = []; + let documentCount = 0; + if (InvertedIndex.validateFile(fileContent, fileName)) { + Object.keys(fileContent).forEach(() => { + words.push(InvertedIndex + .getDocumentTokens(fileContent, documentCount)); + documentCount += 1; + }); + this.filesIndexed[fileName].documentCount = documentCount; + this.filesIndexed[fileName].index = InvertedIndex.constructIndex(words); + return true; + } + return false; + } + /** + * validateFile if file has content and + * validates the structure of the file uploaded + * @param {object} fileContent - The json data to be validated + * @param {string} fileName - The name of the file being validated + * @return {boolean} - True when document has the right structure + * and False if otherwise + */ + static validateFile(fileContent, fileName) { + try { + if (Object.keys(fileContent).length < 1) { + this.handleError(fileName, + 'File contains no document', true); + } + Object.keys(fileContent).forEach((eachIndex) => { + if (!fileContent[eachIndex].text || !fileContent[eachIndex].title) { + this.handleError(fileName, + 'Incorrect Document Structure', true); + } + }); + return true; + } catch (err) { + return false; + } + } + /** + * getDocumentTokens method gets all the tokens in each document + * and composes an object out of them + * @param {object} documentDetails - contains + * the title and text of the document + * @param {integer} documentCount - the number of the document + * @return {object} containing the document Number and the token + */ + static getDocumentTokens(documentDetails, documentCount) { + const textTokens = InvertedIndex.tokenize( + `${documentDetails[documentCount].text} + ${documentDetails[documentCount].title}` + ); + return { documentCount, textTokens }; + } + + /** + * tokenize: method removes special characters and converts the text to + * lowercase and then returns the array of words + * @param {string} text - the text to be tokenized + * @return {array} array of words in the documents + */ + static tokenize(text) { + let splittedWords = text.replace(/[^A-Za-z\s+]/g, '').trim() + .toLowerCase().split(/\b\s+(?!$)/); + splittedWords = splittedWords.filter(eachWords => eachWords !== ''); + return splittedWords; + } + + /** + * constructIndex method searches through the array of documents objects and + * identifies the words in each + * @param {array} documents - array of objects, each obect is a document + * @return {object} objects of tokens. Each token is a key in the object and + * contains an array of documents in which it was found + */ + static constructIndex(documents) { + const indexWords = {}; + documents.forEach((eachDocument) => { + eachDocument.textTokens.forEach((token) => { + if (!hasProperty.call(indexWords, token)) { + indexWords[token] = []; + } + if (indexWords[token].indexOf(eachDocument.documentCount) === -1) { + indexWords[token].push(eachDocument.documentCount); + } + }); + }); + return indexWords; + } + + /** + * getIndex method returns the indexed words and the documents that were found + * @param {string} fileName - name of the file to get its index + * @return {Object|boolean} the index or false if unable to + */ + getIndex(fileName) { + try { + if (!this.filesIndexed[fileName]) { + this.handleError(fileName, 'File selected not indexed', false); + } + const file = this.filesIndexed[fileName]; + return file.index; + } catch (err) { + return false; + } + } + + /** + * searchIndex searches the indexed words to determine the + * documents that the searchterms can be found + * @param {array} searchTerm - the search query, array of words + * @param {string} fileName - the name of the file to search its index + * @return {object|boolean} it returns boolean if the searchTerm is empty and + * it returns object if it is not. Each index is each searcykeyword. + * Each with an array value of the document index + */ + searchIndex(searchTerm, fileName) { + if ((typeof searchTerm === 'string' && searchTerm.trim() === '') || + searchTerm === undefined) { + return false; + } + const result = []; + if (fileName === 'all') { + Object.keys(this.filesIndexed).forEach((eachFile) => { + result.push({ + indexes: this.getSearchResults(searchTerm, eachFile), + searchedFile: eachFile, + documents: this.getDocuments(eachFile) + }); + }); + } else { + result.push({ + indexes: this.getSearchResults(searchTerm, fileName), + searchedFile: fileName, + documents: this.getDocuments(fileName) + }); + } + return result; + } + + /** + * getSearchResults method checks the index of the file and returns the result + * @param {searchTokens} searchTokens - the search query of one or more words + * @param {string} fileName - the name of the file + * @return {array} result - an array of objects with the found words as keys + */ + getSearchResults(searchTokens, fileName) { + const indexToSearch = this.getIndex(fileName) || {}, result = {}; + const tokens = InvertedIndex.tokenize(searchTokens); + + for (let i = 0; i <= tokens.length; i += 1) { + if (indexToSearch[tokens[i]]) { + result[tokens[i]] = indexToSearch[tokens[i]]; + } + } + return result; + } + + /** + * getDocuments get an array of the documents index e.g [0, 1, 2, 3] + * @param {string} fileName - name of the file to get its document + * @return {array} an array of the documents indexed + */ + getDocuments(fileName) { + const documents = []; + for (let i = 0; i < this.filesIndexed[fileName].documentCount; i += 1) { + documents.push(i); + } + return documents; + } + +} + diff --git a/src/app.js b/src/app.js deleted file mode 100644 index 134c6d3..0000000 --- a/src/app.js +++ /dev/null @@ -1,89 +0,0 @@ - -dPlexApp.controller('dPlexController', ['$scope', 'toastr', ($scope, toastr) => { - $scope.title = 'DPlex - Inverted Index for All'; - // Create an object of the class InvertedIndex - const invertedIndex = new InvertedIndex(); - $scope.uploadedFiles = {}; - $scope.allFlag = false; - $scope.allFilesIndexed = {}; - $scope.uploadSelected = ''; - /* * - $scope.createIndex is executed when the createIndex button is clicked - */ - $scope.createIndex = () => { - // console.log($scope.uploadedFiles); - const fileChoice = $scope.uploadSelected; - if (!fileChoice) { - toastr.info('Select a file to index'); - return false; - } - // If index was created for that file - if (invertedIndex.createIndex($scope - .uploadedFiles[fileChoice].text, fileChoice)) { - // Gets the indexed words - const indexes = invertedIndex.getIndex(fileChoice); - $scope.indexDisplay = true; - $scope.indexed = [ - { - indexes, - documents: invertedIndex.getDocuments(fileChoice), - indexedFile: fileChoice - } - ]; - // Keeps track of files that have been indexed - $scope.allFilesIndexed[fileChoice] = true; - const fileNamesIndexed = Object.keys($scope.allFilesIndexed); - $scope.uploadToSearch = fileNamesIndexed[fileNamesIndexed.length - 1]; - /* To check if two files or above have been indexed - so that an option to search all files can be added*/ - if (Object.keys($scope.allFilesIndexed).length > 1) { - $scope.allFlag = true; - } - } else { - // The file was not indexed because it is invalid; - delete $scope.uploadedFiles[fileChoice]; - toastr.error(invertedIndex.error.message); - } - }; - - $scope.searchIndex = () => { - const fileChoice = $scope.uploadToSearch; - $scope.searchQuery = $scope.searchTerm; - if (!$scope.uploadedFiles.hasOwnProperty(fileChoice) && fileChoice !== 'all') { - toastr.warning('Select a file that has been indexed'); - return false; - } - const result = invertedIndex.searchIndex($scope.searchQuery, fileChoice); - if (!result) { - toastr.error('Invalid search query'); - return false; - } - $scope.indexed = result; - $scope.indexDisplay = false; - }; - /** - * readJson function is used to read the content of a file - * @param {object} dom is an object representing the dom element the change event was attached to - */ - $scope.readJson = (dom) => { - for (let i = 0; i < dom.target.files.length; i += 1) { - const fileDetails = dom.target.files[i]; - // check if filename ends in json - invertedIndex.readFile(fileDetails).then((content) => { - $scope.fileContent = content; - $scope.uploadedFiles[fileDetails.name] = {}; - $scope.uploadedFiles[fileDetails.name].text = $scope.fileContent; - const fileNames = Object.keys($scope.uploadedFiles); - $scope.uploadSelected = fileNames[fileNames.length - 1]; - toastr.success('File Uploaded successfully'); - // to make angular update the view - $scope.$apply(); - }).catch((error) => { - toastr.error(`File Error: ${error}`); - }); - } - }; - - $scope.isEmpty = value => Object.keys(value).length === 0; - document.getElementById('uploadfile').addEventListener('change', $scope.readJson); -}]); diff --git a/index.html b/src/index.html similarity index 92% rename from index.html rename to src/index.html index 18dc276..8dece34 100644 --- a/index.html +++ b/src/index.html @@ -11,11 +11,11 @@ - + - - + + @@ -99,7 +99,8 @@

Showing index for {{details.indexedFile}} Search result for "{{searchQuery}}" in "{{details.searchedFile}}"

- +
+
@@ -108,11 +109,12 @@

Tokens Document {{i+1}}
{{key}} - X +
+
@@ -156,9 +158,9 @@

How to use

- + - - + + diff --git a/src/invertedindex.js b/src/invertedindex.js deleted file mode 100644 index 9ff60a5..0000000 --- a/src/invertedindex.js +++ /dev/null @@ -1,233 +0,0 @@ - -const hasProperty = Object.prototype.hasOwnProperty; -let instance = null; - -/** - * @class InvertedIndex class - * Contains methods for InvertedIndex -*/ -class InvertedIndex { - /** - * constructor method ensures that there is only - * one instance of the class - * @return {object} - Instance of the class - */ - constructor() { - if (!instance) { - instance = this; - this.filesIndexed = {}; - this.inputData = {}; - this.error = {}; - } - return instance; - } - - /** - * readFile function is used to get all the index - * @param {object} inputData - the json data to index - * @return {reject(false)} - When file is of bad extent of - * invalid json format - * @return {resolve(true)} - When file is of the right extension structure - */ - readFile(inputData) { - return new Promise((resolve, reject) => { - if (!inputData.name.match(/\.json$/)) { - return reject(false); - } - const readFile = new FileReader(); - readFile.readAsText(inputData); - readFile.onload = (file) => { - const content = file.target.result; - try { - return resolve(JSON.parse(content)); - } catch (exception) { - return reject(false); - } - }; - }); - } - - /** - * handleError handles error - * @param {string} fileName - Name of file being indexed or searched - * @param {string} errorMessage - Error message to be displayed - * @param {boolean} errorStatus - True or False - */ - handleError(fileName, errorMessage, errorStatus){ - delete this.filesIndexed[fileName]; - this.error.status = errorStatus; - this.error.message = errorMessage; - throw this.error; - } - - /** - * createIndex gets the json ready for indexing by tokenizing statements - * @param {object} inputData - the json data to index - * @param {string} filename - the name of the file to be indexed - * @return {boolean} - true or false if the createIndex was successful - */ - createIndex(inputData, filename) { - this.filesIndexed[filename] = {}; - const words = []; - let documentNum = 0; - try { - if (Object.keys(inputData).length < 1) { - this.handleError(filename, 'File contains no document', true); - } - Object.keys(inputData).forEach((eachIndex) => { - if (!this.validateFile(inputData[eachIndex])) { - this.handleError(filename, 'Incorrect Document Structure', true); - } - words.push(this.getDocumentTokens(inputData, documentNum)); - documentNum += 1; - }); - this.filesIndexed[filename].numOfDocs = documentNum; - this.filesIndexed[filename].index = this.constructIndex(words); - return true; - } catch (err) { - if (this.error.status) { - return false; - } - } - } - /** - * validateFile validates the structure of the file uploaded - * @param {object} docToValidate - The json data to be validated - * @return {boolean} - True when document has the right structure - * and False if otherwise - */ - validateFile(docToValidate) { - if (!docToValidate.text || !docToValidate.title) { - return false; - } - return true; - } - /** - * getDocumentTokens method gets all the tokens in each document - * and composes an object out of them - * @param {object} docDetails - contains the title and text of the document - * @param {integer} documentNum - the number of the document - * @return {object} containing the document Number and the token - */ - getDocumentTokens(docDetails, documentNum) { - const textTokens = this - .tokenize( - `${docDetails[documentNum].text} ${docDetails[documentNum].title}` - ); - return { documentNum, textTokens }; - } - - /** - * tokenize: method removes special characters and converts the text to - * lowercase and then returns the array of words - * @param {string} text- the text to be tokenized - * @return {array} array of words in the documents - */ - tokenize(text) { - text = text.replace(/[^A-Za-z\s-]/g, '').trim(); - return text.toLowerCase().split(' '); - } - - /** - * constructIndex method searches through the array of documents objects and - * dentifies the words in each - * @param {array} documents - array of objects, each obect is a document - * @return {object} objects of tokens. Each token is a key in the object and - * contains an array of documents in which it was found - */ - constructIndex(documents) { - const indexWords = {}; - documents.forEach((eachDoc) => { - eachDoc.textTokens.forEach((token) => { - if (!hasProperty.call(indexWords, token)) { - indexWords[token] = []; - } - if (indexWords[token].indexOf(eachDoc.documentNum) === -1) { - indexWords[token].push(eachDoc.documentNum); - } - }); - }); - return indexWords; - } - - /** - * getIndex method returns the indexed words and the documents that were found - * @param {string} filename - name of the file to get its index - * @return {Object|boolean} the index or false if unable to - */ - getIndex(filename) { - try { - if (!this.filesIndexed[filename]) { - this.handleError(filename, 'File selected not indexed', false) - } - const file = this.filesIndexed[filename]; - return file.index; - } catch (err) { - return this.error.status; - } - } - - /** - * searchIndex searches the indexed words to determine the - * documents that the searchterms can be found - * @param {array} searchTerm - the search query, array of words - * @param {string} filename - the name of the file to search its index - * @return {object|boolean} it returns boolean if the searchTerm is empty and - * it returns object if it is not. Each index is each searcykeyword. - * Each with an array value of the document index - */ - searchIndex(searchTerm, filename) { - if ((typeof searchTerm === 'string' && searchTerm.trim() === '') || - (typeof searchTerm === 'object' && searchTerm.length === 0) || - searchTerm === undefined) { - return false; - } - const result = []; - if (filename === 'all') { - Object.keys(this.filesIndexed).forEach((eachFile) => { - result.push({ - indexes: this.getSearchResults(searchTerm, eachFile), - searchedFile: eachFile, - documents: this.getDocuments(eachFile) - }); - }); - } else { - result.push({ - indexes: this.getSearchResults(searchTerm, filename), - searchedFile: filename, - documents: this.getDocuments(filename) - }); - } - return result; - } - - /** - * getSearchResults method checks the index of the file and returns the result - * @param searchTokens {searchTokens} - the search query of one or more words - * @param filename {string} - the name of the file - * @return {object} - an object with the found words as keys - */ - getSearchResults(searchTokens, filename) { - const indexToSearch = this.getIndex(filename), result = {}; - this.tokenize(searchTokens).forEach((eachSearchWord) => { - if (indexToSearch[eachSearchWord]) { - result[eachSearchWord] = indexToSearch[eachSearchWord]; - } - }); - return result; - } - - /** - * getDocuments get an array of the documents index e.g [0, 1, 2, 3] - * @param {filename} - name of the file to get its document - * @return {array} an array of the documents index - */ - getDocuments(filename) { - const docs = []; - for (let i = 0; i < this.filesIndexed[filename].numOfDocs; i++) { - docs.push(i); - } - return docs; - } - -} diff --git a/css/animate.css b/src/public/css/animate.css similarity index 100% rename from css/animate.css rename to src/public/css/animate.css diff --git a/css/bootstrap.css b/src/public/css/bootstrap.css similarity index 100% rename from css/bootstrap.css rename to src/public/css/bootstrap.css diff --git a/css/style.css b/src/public/css/style.css similarity index 92% rename from css/style.css rename to src/public/css/style.css index 884163a..a0e920d 100644 --- a/css/style.css +++ b/src/public/css/style.css @@ -33,3 +33,8 @@ .padding10{ padding: 10px; } + +#myTable{ + z-index: 1; + overflow-y: scroll; +} \ No newline at end of file diff --git a/src/public/js/app.js b/src/public/js/app.js new file mode 100644 index 0000000..3b7ca1d --- /dev/null +++ b/src/public/js/app.js @@ -0,0 +1,94 @@ + +dPlexApp.controller('dPlexController', + ['$scope', 'toastr', ($scope, toastr) => { + $scope.title = 'DPlex - Inverted Index for All'; + // Create an object of the class InvertedIndex + const invertedIndex = new InvertedIndex(); + $scope.uploadedFiles = {}; + $scope.allFlag = false; + $scope.allFilesIndexed = {}; + $scope.uploadSelected = ''; + /* * + $scope.createIndex is executed when the createIndex button is clicked + */ + $scope.createIndex = () => { + // console.log($scope.uploadedFiles); + const fileChoice = $scope.uploadSelected; + if (!fileChoice) { + toastr.info('Select a file to index'); + return false; + } + // If index was created for that file + if (invertedIndex.createIndex($scope + .uploadedFiles[fileChoice].text, fileChoice)) { + // Gets the indexed words + const indexes = invertedIndex.getIndex(fileChoice); + $scope.indexDisplay = true; + $scope.indexed = [ + { + indexes, + documents: invertedIndex.getDocuments(fileChoice), + indexedFile: fileChoice + } + ]; + // Keeps track of files that have been indexed + $scope.allFilesIndexed[fileChoice] = true; + const fileNamesIndexed = Object.keys($scope.allFilesIndexed); + $scope.uploadToSearch = fileNamesIndexed[fileNamesIndexed.length - 1]; + /* To check if two files or above have been indexed + so that an option to search all files can be added*/ + if (Object.keys($scope.allFilesIndexed).length > 1) { + $scope.allFlag = true; + } + } else { + // The file was not indexed because it is invalid; + delete $scope.uploadedFiles[fileChoice]; + toastr.error(invertedIndex.error.message); + } + }; + + $scope.searchIndex = () => { + const fileChoice = $scope.uploadToSearch; + $scope.searchQuery = $scope.searchTerm; + if (!$scope.uploadedFiles + .hasOwnProperty(fileChoice) && fileChoice !== 'all') { + toastr.warning('Select a file that has been indexed'); + return false; + } + const result = invertedIndex.searchIndex($scope.searchQuery, fileChoice); + if (!result) { + toastr.error('Invalid search query'); + return false; + } + $scope.indexed = result; + $scope.indexDisplay = false; + }; + /** + * readJson function is used to read the content of a file + * @param {object} dom - is an object + * representing the dom element the change event was attached to + * @return {null} - Does not return anything + */ + $scope.readJson = (dom) => { + for (let i = 0; i < dom.target.files.length; i += 1) { + const fileDetails = dom.target.files[i]; + // check if filename ends in json + InvertedIndex.readFile(fileDetails).then((content) => { + $scope.fileContent = content; + $scope.uploadedFiles[fileDetails.name] = {}; + $scope.uploadedFiles[fileDetails.name].text = $scope.fileContent; + const fileNames = Object.keys($scope.uploadedFiles); + $scope.uploadSelected = fileNames[fileNames.length - 1]; + toastr.success('File Uploaded successfully'); + // to make angular update the view + $scope.$apply(); + }).catch((error) => { + toastr.error(`File Error: ${error}`); + }); + } + }; + + $scope.isEmpty = value => Object.keys(value).length === 0; + document.getElementById('uploadfile') + .addEventListener('change', $scope.readJson); + }]); diff --git a/src/module.js b/src/public/js/module.js similarity index 100% rename from src/module.js rename to src/public/js/module.js