├── babel.config.js ├── .gitignore ├── benchmarks ├── .gitignore ├── trace.js ├── package.json ├── index.html ├── README.md ├── test.js ├── yarn.lock ├── create-index.js ├── search.js ├── regression-test.js └── books.json ├── .flowconfig ├── source ├── Sanitizer │ ├── index.js │ ├── CaseSensitiveSanitizer.js │ ├── Sanitizer.js │ ├── LowerCaseSanitizer.js │ ├── LowerCaseSanitizer.test.js │ └── CaseSensitiveSanitizer.test.js ├── SearchIndex │ ├── index.js │ ├── SearchIndex.js │ ├── UnorderedSearchIndex.test.js │ ├── UnorderedSearchIndex.js │ ├── TfIdfSearchIndex.js │ └── TfIdfSearchIndex.test.js ├── Tokenizer │ ├── index.js │ ├── Tokenizer.js │ ├── SimpleTokenizer.js │ ├── StemmingTokenizer.test.js │ ├── StopWordsTokenizer.js │ ├── SimpleTokenizer.test.js │ ├── StemmingTokenizer.js │ └── StopWordsTokenizer.test.js ├── IndexStrategy │ ├── index.js │ ├── ExactWordIndexStrategy.js │ ├── PrefixIndexStrategy.js │ ├── ExactWordIndexStrategy.test.js │ ├── IndexStrategy.js │ ├── AllSubstringsIndexStrategy.js │ ├── PrefixIndexStrategy.test.js │ └── AllSubstringsIndexStrategy.test.js ├── getNestedFieldValue.js ├── index.js ├── StopWordsMap.js ├── TokenHighlighter.test.js ├── TokenHighlighter.js ├── Search.test.js └── Search.js ├── rollup.config.js ├── LICENSE.md ├── package.json ├── CHANGELOG.md └── README.md /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/env", { loose: true }], "@babel/flow"] 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.bower 3 | /.idea 4 | /bower_components 5 | /node_modules 6 | /npm-debug.log 7 | /dist 8 | -------------------------------------------------------------------------------- /benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.bower 3 | /.idea 4 | /bower_components 5 | /node_modules 6 | /npm-debug.log 7 | /dist 8 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | untyped-type-import=error 9 | untyped-import=error 10 | 11 | [options] 12 | 13 | [strict] 14 | -------------------------------------------------------------------------------- /source/Sanitizer/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export { CaseSensitiveSanitizer } from './CaseSensitiveSanitizer'; 4 | export { LowerCaseSanitizer } from './LowerCaseSanitizer'; 5 | 6 | export type { ISanitizer } from './Sanitizer'; 7 | -------------------------------------------------------------------------------- /source/SearchIndex/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export { TfIdfSearchIndex } from './TfIdfSearchIndex'; 4 | export { UnorderedSearchIndex } from './UnorderedSearchIndex'; 5 | 6 | export type { ISearchIndex } from './SearchIndex'; 7 | -------------------------------------------------------------------------------- /source/Tokenizer/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export { SimpleTokenizer } from './SimpleTokenizer'; 4 | export { StemmingTokenizer } from './StemmingTokenizer'; 5 | export { StopWordsTokenizer } from './StopWordsTokenizer'; 6 | 7 | export type { ITokenizer } from './Tokenizer'; 8 | -------------------------------------------------------------------------------- /source/IndexStrategy/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export { AllSubstringsIndexStrategy } from './AllSubstringsIndexStrategy'; 4 | export { ExactWordIndexStrategy } from './ExactWordIndexStrategy'; 5 | export { PrefixIndexStrategy } from './PrefixIndexStrategy'; 6 | 7 | export type { IIndexStrategy } from './IndexStrategy'; 8 | -------------------------------------------------------------------------------- /source/Sanitizer/CaseSensitiveSanitizer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ISanitizer } from './Sanitizer'; 4 | 5 | /** 6 | * Enforces case-sensitive text matches. 7 | */ 8 | export class CaseSensitiveSanitizer implements ISanitizer { 9 | 10 | /** 11 | * @inheritDocs 12 | */ 13 | sanitize(text : string) : string { 14 | return text ? text.trim() : ''; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /source/IndexStrategy/ExactWordIndexStrategy.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { IIndexStrategy } from './IndexStrategy'; 4 | 5 | /** 6 | * Indexes for exact word matches. 7 | */ 8 | export class ExactWordIndexStrategy implements IIndexStrategy { 9 | 10 | /** 11 | * @inheritDocs 12 | */ 13 | expandToken(token : string) : Array { 14 | return token ? [token] : []; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /source/Sanitizer/Sanitizer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * A sanitizer helps convert searchable field text and user query text to a format that can be easily compared. Among 5 | * other things, this often involves operations like trimming leading and trailing whitespace. 6 | */ 7 | export interface ISanitizer { 8 | 9 | /** 10 | * @param text 11 | * @return Sanitized text 12 | */ 13 | sanitize(text : string) : string; 14 | }; 15 | -------------------------------------------------------------------------------- /source/Tokenizer/Tokenizer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * A tokenizer converts a string of text (e.g. "the boy") to a set of tokens (e.g. "the", "boy"). These tokens are used 5 | * for indexing and searching purposes. 6 | */ 7 | export interface ITokenizer { 8 | 9 | /** 10 | * @param text String of text (e.g. "the boy") 11 | * @return Array of text tokens (e.g. "the", "boy") 12 | */ 13 | tokenize(text : string) : Array; 14 | }; 15 | -------------------------------------------------------------------------------- /source/Sanitizer/LowerCaseSanitizer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ISanitizer } from './Sanitizer'; 4 | 5 | /** 6 | * Sanitizes text by converting to a locale-friendly lower-case version and triming leading and trailing whitespace. 7 | */ 8 | export class LowerCaseSanitizer implements ISanitizer { 9 | 10 | /** 11 | * @inheritDocs 12 | */ 13 | sanitize(text : string) : string { 14 | return text ? text.toLocaleLowerCase().trim() : ''; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /source/Tokenizer/SimpleTokenizer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ITokenizer } from './Tokenizer'; 4 | 5 | var REGEX = /[^a-zа-яё0-9\-']+/i; 6 | 7 | /** 8 | * Simple tokenizer that splits strings on whitespace characters and returns an array of all non-empty substrings. 9 | */ 10 | export class SimpleTokenizer implements ITokenizer { 11 | 12 | /** 13 | * @inheritDocs 14 | */ 15 | tokenize(text : string) : Array { 16 | return text 17 | .split(REGEX) 18 | .filter( 19 | (text) => text // Filter empty tokens 20 | ); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /benchmarks/trace.js: -------------------------------------------------------------------------------- 1 | var { Search, AllSubstringsIndexStrategy, ExactWordIndexStrategy } = require('../dist/umd/js-search'); 2 | 3 | function identity(text) { 4 | return text; 5 | } 6 | 7 | var search = new Search('id'); 8 | search.indexStrategy = new AllSubstringsIndexStrategy(); 9 | search.addIndex('text'); 10 | 11 | 12 | for (var i = 0; i < 10000; i++) { 13 | search.addDocuments([ 14 | { id: 1, text: 'foo bar' }, 15 | { id: 2, text: 'bar baz' }, 16 | { id: 3, text: 'baz foo' } 17 | ]); 18 | 19 | search.search('foo'); 20 | search.search('bar'); 21 | search.search('baz'); 22 | } 23 | -------------------------------------------------------------------------------- /source/getNestedFieldValue.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Find and return a nested object value. 5 | * 6 | * @param object to crawl 7 | * @param path Property path 8 | * @returns {any} 9 | */ 10 | export default function getNestedFieldValue(object : Object, path : Array): any { 11 | path = path || []; 12 | object = object || {}; 13 | 14 | var value = object; 15 | 16 | // walk down the property path 17 | for (var i = 0; i < path.length; i++) { 18 | value = value[path[i]]; 19 | 20 | if (value == null) { 21 | return null; 22 | } 23 | } 24 | 25 | return value; 26 | } 27 | -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export { 4 | AllSubstringsIndexStrategy, 5 | ExactWordIndexStrategy, 6 | PrefixIndexStrategy 7 | } from './IndexStrategy/index'; 8 | 9 | export { 10 | CaseSensitiveSanitizer, 11 | LowerCaseSanitizer 12 | } from './Sanitizer/index'; 13 | 14 | export { 15 | TfIdfSearchIndex, 16 | UnorderedSearchIndex 17 | } from './SearchIndex/index'; 18 | 19 | export { 20 | SimpleTokenizer, 21 | StemmingTokenizer, 22 | StopWordsTokenizer 23 | } from './Tokenizer/index'; 24 | 25 | export { Search } from './Search'; 26 | export { StopWordsMap } from './StopWordsMap'; 27 | export { TokenHighlighter } from './TokenHighlighter'; 28 | -------------------------------------------------------------------------------- /benchmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-search-benchmark", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "create-index.js", 6 | "dependencies": { 7 | "js-search": "^1.3.5", 8 | "beautify-benchmark": "^0.2.4", 9 | "lodash": "^4.17.19", 10 | "benchmark": "^2.1.3", 11 | "lunr": "^0.7.2" 12 | }, 13 | "devDependencies": { 14 | "beautify-benchmark": "^0.2.4", 15 | "benchmark": "^2.1.3", 16 | "js-search": "^1.3.5", 17 | "lunr": "^0.7.2", 18 | "microtime": "^2.1.2" 19 | }, 20 | "scripts": { 21 | "test": "echo \"Error: no test specified\" && exit 1" 22 | }, 23 | "author": "", 24 | "license": "ISC" 25 | } 26 | -------------------------------------------------------------------------------- /source/IndexStrategy/PrefixIndexStrategy.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { IIndexStrategy } from './IndexStrategy'; 4 | 5 | /** 6 | * Indexes for prefix searches (e.g. the term "cat" is indexed as "c", "ca", and "cat" allowing prefix search lookups). 7 | */ 8 | export class PrefixIndexStrategy implements IIndexStrategy { 9 | 10 | /** 11 | * @inheritDocs 12 | */ 13 | expandToken(token : string) : Array { 14 | var expandedTokens = []; 15 | var string = ''; 16 | 17 | for (var i = 0, length = token.length; i < length; ++i) { 18 | string += token.charAt(i); 19 | expandedTokens.push(string); 20 | } 21 | 22 | return expandedTokens; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /benchmarks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /source/IndexStrategy/ExactWordIndexStrategy.test.js: -------------------------------------------------------------------------------- 1 | import { ExactWordIndexStrategy } from './ExactWordIndexStrategy'; 2 | 3 | describe('ExactWordIndexStrategy', function() { 4 | var indexStrategy; 5 | 6 | beforeEach(function() { 7 | indexStrategy = new ExactWordIndexStrategy(); 8 | }); 9 | 10 | it('should not expand empty tokens', function() { 11 | var expandedTokens = indexStrategy.expandToken(''); 12 | 13 | expect(expandedTokens.length).toEqual(0); 14 | }); 15 | 16 | it('should not expand tokens', function() { 17 | var expandedTokens = indexStrategy.expandToken('cat'); 18 | 19 | expect(expandedTokens.length).toEqual(1); 20 | expect(expandedTokens).toContain('cat'); 21 | }); 22 | }); -------------------------------------------------------------------------------- /source/IndexStrategy/IndexStrategy.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Indexing is the most important part of search. A proper index allows query terms to be matched to related results in 5 | * an efficient manner. 6 | * 7 | *

The simplest index strategy is to only index on a token. Other strategies can often be more useful though. For 8 | * example a prefix indexing strategy (e.g. "cat" to "c", "ca", "cat") allows users to search as they type, gradually 9 | * refining results as the search query becomes more detailed. 10 | */ 11 | export interface IIndexStrategy { 12 | 13 | /** 14 | * @param token A search token 15 | * @return Expanded search token 16 | */ 17 | expandToken(token : string) : Array; 18 | }; 19 | -------------------------------------------------------------------------------- /source/IndexStrategy/AllSubstringsIndexStrategy.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { IIndexStrategy } from './IndexStrategy'; 4 | 5 | /** 6 | * Indexes for all substring searches (e.g. the term "cat" is indexed as "c", "ca", "cat", "a", "at", and "t"). 7 | */ 8 | export class AllSubstringsIndexStrategy implements IIndexStrategy { 9 | 10 | /** 11 | * @inheritDocs 12 | */ 13 | expandToken(token : string) : Array { 14 | var expandedTokens = []; 15 | var string; 16 | 17 | for (var i = 0, length = token.length; i < length; ++i) { 18 | string = ''; 19 | 20 | for (var j = i; j < length; ++j) { 21 | string += token.charAt(j); 22 | expandedTokens.push(string); 23 | } 24 | } 25 | 26 | return expandedTokens; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /source/Tokenizer/StemmingTokenizer.test.js: -------------------------------------------------------------------------------- 1 | import { SimpleTokenizer } from './SimpleTokenizer'; 2 | import { StemmingTokenizer } from './StemmingTokenizer'; 3 | 4 | describe('StemmingTokenizer', function() { 5 | var tokenizer; 6 | 7 | beforeEach(function() { 8 | var stemmingFunction = function(text) { 9 | if (text === 'cats') { 10 | return 'cat'; 11 | } else { 12 | return text; 13 | } 14 | }; 15 | 16 | tokenizer = new StemmingTokenizer(stemmingFunction, new SimpleTokenizer()); 17 | }); 18 | 19 | it('should handle empty values', function() { 20 | expect(tokenizer.tokenize('')).toEqual([]); 21 | expect(tokenizer.tokenize(' ')).toEqual([]); 22 | }); 23 | 24 | it('should convert words to stems', function() { 25 | expect(tokenizer.tokenize('the cats')).toEqual(['the', 'cat']); 26 | }); 27 | }); -------------------------------------------------------------------------------- /source/SearchIndex/SearchIndex.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * A search index stores documents in such a way as to enable quick lookup against one or more tokens. 5 | */ 6 | export interface ISearchIndex { 7 | 8 | /** 9 | * Track the specified document and token association. 10 | * 11 | * @param token 12 | * @param uid 13 | * @param document 14 | * @return Sanitized text 15 | */ 16 | indexDocument( 17 | token : string, 18 | uid : string, 19 | document : Object 20 | ) : void; 21 | 22 | /** 23 | * Return all documents that match the specified tokens. 24 | * 25 | * @param tokens Tokenized query (eg "the boy" query becomes ["the", "boy"] tokens) 26 | * @param corpus All document in search corpus 27 | * @return Array of matching documents 28 | */ 29 | search( 30 | tokens : Array, 31 | corpus : Array 32 | ) : Array; 33 | }; 34 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import { terser } from "rollup-plugin-terser"; 3 | 4 | const emitModulePackageFile = () => { 5 | return { 6 | name: "emit-module-package-file", 7 | generateBundle() { 8 | this.emitFile({ 9 | type: "asset", 10 | fileName: "package.json", 11 | source: `{"type":"module"}` 12 | }); 13 | } 14 | }; 15 | }; 16 | 17 | export default { 18 | input: "./source/index.js", 19 | output: [ 20 | { 21 | format: "umd", 22 | file: "dist/umd/js-search.js", 23 | name: "JsSearch" 24 | }, 25 | { 26 | format: "umd", 27 | file: "dist/umd/js-search.min.js", 28 | name: "JsSearch", 29 | plugins: [terser()] 30 | }, 31 | { 32 | format: "esm", 33 | file: "dist/esm/js-search.js", 34 | plugins: [emitModulePackageFile()] 35 | } 36 | ], 37 | plugins: [babel()] 38 | }; 39 | -------------------------------------------------------------------------------- /source/Tokenizer/StopWordsTokenizer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ITokenizer } from './Tokenizer'; 4 | 5 | import { StopWordsMap } from '../StopWordsMap'; 6 | 7 | /** 8 | * Stop words are very common (e.g. "a", "and", "the") and are often not semantically meaningful in the context of a 9 | * search. This tokenizer removes stop words from a set of tokens before passing the remaining tokens along for 10 | * indexing or searching purposes. 11 | */ 12 | export class StopWordsTokenizer implements ITokenizer { 13 | _tokenizer : ITokenizer; 14 | 15 | /** 16 | * Constructor. 17 | * 18 | * @param decoratedIndexStrategy Index strategy to be run after all stop words have been removed. 19 | */ 20 | constructor(decoratedTokenizer : ITokenizer) { 21 | this._tokenizer = decoratedTokenizer; 22 | } 23 | 24 | /** 25 | * @inheritDocs 26 | */ 27 | tokenize(text : string) : Array { 28 | return this._tokenizer.tokenize(text) 29 | .filter( 30 | (token) => !StopWordsMap[token] 31 | ); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /source/IndexStrategy/PrefixIndexStrategy.test.js: -------------------------------------------------------------------------------- 1 | import { PrefixIndexStrategy } from './PrefixIndexStrategy'; 2 | 3 | describe('PrefixIndexStrategy', function() { 4 | var indexStrategy; 5 | 6 | beforeEach(function() { 7 | indexStrategy = new PrefixIndexStrategy(); 8 | }); 9 | 10 | it('should not expand empty tokens', function() { 11 | var expandedTokens = indexStrategy.expandToken(''); 12 | 13 | expect(expandedTokens.length).toEqual(0); 14 | }); 15 | 16 | it('should not expand single character tokens', function() { 17 | var expandedTokens = indexStrategy.expandToken('a'); 18 | 19 | expect(expandedTokens.length).toEqual(1); 20 | expect(expandedTokens).toContain('a'); 21 | }); 22 | 23 | it('should expand multi-character tokens', function() { 24 | var expandedTokens = indexStrategy.expandToken('cat'); 25 | 26 | expect(expandedTokens.length).toEqual(3); 27 | expect(expandedTokens).toContain('c'); 28 | expect(expandedTokens).toContain('ca'); 29 | expect(expandedTokens).toContain('cat'); 30 | }); 31 | }); -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | These benchmark tests measure real usage of js-search. 2 | They enable simple performance comparisons between the latest released build and the local build. 3 | To run a benchmark: 4 | 5 | ```bash 6 | cd /path/to/js-search 7 | yarn install 8 | 9 | # Build local js-search (to incorporate any changes you've made) 10 | npm run build 11 | 12 | cd ./benchmarks 13 | yarn install 14 | 15 | # Run a benchmark of your choice 16 | # eg Compare your local build of js-search to the latest released build 17 | node ./regression-test.js 18 | ``` 19 | 20 | You can also compare two local builds of js-search: 21 | 22 | ```bash 23 | # Assumes you've run `yarn install` in both directories prior 24 | 25 | cd /path/to/js-search 26 | 27 | # Make some changes and then build js-search 28 | npm run build 29 | 30 | # Move your build into the benchmarks folder as the 'latest' 31 | cp -r ./dist ./benchmarks/node_modules/js-search/ 32 | 33 | # Make more changes and then re-build js-search 34 | npm run build 35 | 36 | # Compare your most recent build to the previous one 37 | cd ./benchmarks 38 | node ./regression-test.js 39 | ``` -------------------------------------------------------------------------------- /source/Sanitizer/LowerCaseSanitizer.test.js: -------------------------------------------------------------------------------- 1 | import { LowerCaseSanitizer } from './LowerCaseSanitizer'; 2 | 3 | describe('LowerCaseSanitizer', function() { 4 | var sanitizer; 5 | 6 | beforeEach(function() { 7 | sanitizer = new LowerCaseSanitizer(); 8 | }); 9 | 10 | it('should handle falsy values', function() { 11 | expect(sanitizer.sanitize(null)).toEqual(''); 12 | expect(sanitizer.sanitize(undefined)).toEqual(''); 13 | expect(sanitizer.sanitize(false)).toEqual(''); 14 | }); 15 | 16 | it('should handle empty strings', function() { 17 | expect(sanitizer.sanitize('')).toEqual(''); 18 | }); 19 | 20 | it('should handle whitespace-only strings', function() { 21 | expect(sanitizer.sanitize(' ')).toEqual(''); 22 | }); 23 | 24 | it('should handle leading and trailing whitespace', function() { 25 | expect(sanitizer.sanitize(' a')).toEqual('a'); 26 | expect(sanitizer.sanitize('b ')).toEqual('b'); 27 | expect(sanitizer.sanitize(' c ')).toEqual('c'); 28 | }); 29 | 30 | it('should convert uppercase to lower case', function() { 31 | expect(sanitizer.sanitize('AbC')).toEqual('abc'); 32 | }); 33 | }); -------------------------------------------------------------------------------- /source/Sanitizer/CaseSensitiveSanitizer.test.js: -------------------------------------------------------------------------------- 1 | import { CaseSensitiveSanitizer } from './CaseSensitiveSanitizer'; 2 | 3 | describe('CaseSensitiveSanitizer', function() { 4 | var sanitizer; 5 | 6 | beforeEach(function() { 7 | sanitizer = new CaseSensitiveSanitizer(); 8 | }); 9 | 10 | it('should handle falsy values', function() { 11 | expect(sanitizer.sanitize(null)).toEqual(''); 12 | expect(sanitizer.sanitize(undefined)).toEqual(''); 13 | expect(sanitizer.sanitize(false)).toEqual(''); 14 | }); 15 | 16 | it('should handle empty strings', function() { 17 | expect(sanitizer.sanitize('')).toEqual(''); 18 | }); 19 | 20 | it('should handle whitespace-only strings', function() { 21 | expect(sanitizer.sanitize(' ')).toEqual(''); 22 | }); 23 | 24 | it('should handle leading and trailing whitespace', function() { 25 | expect(sanitizer.sanitize(' a')).toEqual('a'); 26 | expect(sanitizer.sanitize('b ')).toEqual('b'); 27 | expect(sanitizer.sanitize(' c ')).toEqual('c'); 28 | }); 29 | 30 | it('should not modify case', function() { 31 | expect(sanitizer.sanitize('AbC')).toEqual('AbC'); 32 | }); 33 | }); -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Brian Vaughn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /source/IndexStrategy/AllSubstringsIndexStrategy.test.js: -------------------------------------------------------------------------------- 1 | import { AllSubstringsIndexStrategy } from './AllSubstringsIndexStrategy'; 2 | 3 | describe('AllSubstringsIndexStrategy', function() { 4 | var indexStrategy; 5 | 6 | beforeEach(function() { 7 | indexStrategy = new AllSubstringsIndexStrategy(); 8 | }); 9 | 10 | it('should not expand empty tokens', function() { 11 | var expandedTokens = indexStrategy.expandToken(''); 12 | 13 | expect(expandedTokens.length).toEqual(0); 14 | }); 15 | 16 | it('should not expand single character tokens', function() { 17 | var expandedTokens = indexStrategy.expandToken('a'); 18 | 19 | expect(expandedTokens.length).toEqual(1); 20 | expect(expandedTokens).toContain('a'); 21 | }); 22 | 23 | it('should expand multi-character tokens', function() { 24 | var expandedTokens = indexStrategy.expandToken('cat'); 25 | 26 | expect(expandedTokens.length).toEqual(6); 27 | expect(expandedTokens).toContain('c'); 28 | expect(expandedTokens).toContain('ca'); 29 | expect(expandedTokens).toContain('cat'); 30 | expect(expandedTokens).toContain('a'); 31 | expect(expandedTokens).toContain('at'); 32 | expect(expandedTokens).toContain('t'); 33 | }); 34 | }); -------------------------------------------------------------------------------- /benchmarks/test.js: -------------------------------------------------------------------------------- 1 | const Benchmark = require('benchmark'); 2 | const bb = require('beautify-benchmark'); 3 | 4 | function doThing({ 5 | foo, 6 | bar, 7 | baz, 8 | qux 9 | }) { 10 | return foo + bar + baz + qux; 11 | } 12 | function doThingAlt(foo, bar, baz, qux) { 13 | return foo + bar + baz + qux; 14 | } 15 | 16 | new Benchmark.Suite() 17 | .on('cycle', (event) => { 18 | bb.add(event.target); 19 | }) 20 | .on('complete', () => { 21 | bb.log(); 22 | }) 23 | .add('recreate', () => { 24 | for (var i = 1000; i--;) { 25 | doThing({ 26 | // foo: Math.random(), 27 | // bar: Math.random(), 28 | // baz: Math.random(), 29 | qux: Math.random() 30 | }); 31 | } 32 | }) 33 | .add('recycle', () => { 34 | var doThingParams = { 35 | foo: null, 36 | bar: null, 37 | baz: null, 38 | qux: null 39 | } 40 | for (var i = 1000; i--;) { 41 | doThingParams.foo = Math.random() 42 | doThingParams.bar = Math.random() 43 | doThingParams.baz = Math.random() 44 | doThingParams.qux = Math.random() 45 | doThing(doThingParams); 46 | } 47 | }) 48 | .add('unnamed', () => { 49 | for (var i = 1000; i--;) { 50 | doThingAlt(Math.random(), Math.random(), Math.random(), Math.random()); 51 | } 52 | }) 53 | .run({ 'async': true }); -------------------------------------------------------------------------------- /source/SearchIndex/UnorderedSearchIndex.test.js: -------------------------------------------------------------------------------- 1 | import { Search } from '../Search'; 2 | import { UnorderedSearchIndex } from './UnorderedSearchIndex'; 3 | 4 | describe('Search', function() { 5 | var documents, search; 6 | 7 | beforeEach(function() { 8 | search = new Search('uid'); 9 | search.searchIndex = new UnorderedSearchIndex(); 10 | search.addIndex('title'); 11 | 12 | var titles = [ 13 | 'this document is about node.', 14 | 'this document is about ruby.', 15 | 'this document is about ruby and node.', 16 | 'this document is about node. it has node examples' 17 | ]; 18 | 19 | documents = []; 20 | for (var i = 0, length = titles.length; i < length; ++i) { 21 | var document = { 22 | uid: i, 23 | title: titles[i] 24 | }; 25 | 26 | documents.push(document); 27 | search.addDocument(document); 28 | } 29 | }); 30 | 31 | var validateSearchResults = function(results, expectedDocuments) { 32 | expect(results.length).toBe(expectedDocuments.length); 33 | expectedDocuments.forEach(function(document) { 34 | expect(results).toContain(document); 35 | }); 36 | }; 37 | 38 | it('should return documents matching search tokens', function() { 39 | var results = search.search('node'); 40 | 41 | validateSearchResults(results, [documents[0], documents[2], documents[3]]); 42 | }); 43 | }); -------------------------------------------------------------------------------- /source/Tokenizer/SimpleTokenizer.test.js: -------------------------------------------------------------------------------- 1 | import { SimpleTokenizer } from './SimpleTokenizer'; 2 | 3 | describe('SimpleTokenizer', function() { 4 | var tokenizer 5 | 6 | beforeEach(function() { 7 | tokenizer = new SimpleTokenizer(); 8 | }); 9 | 10 | it('should convert single-token strings', function() { 11 | expect(tokenizer.tokenize('a')).toEqual(['a']); 12 | }); 13 | 14 | it('should convert multi-token strings', function() { 15 | expect(tokenizer.tokenize('a b c')).toEqual(['a', 'b', 'c']); 16 | }); 17 | 18 | it('should not return empty tokens', function() { 19 | expect(tokenizer.tokenize(' a ')).toEqual(['a']); 20 | }); 21 | 22 | it('should remove punctuation', function() { 23 | expect(tokenizer.tokenize('this and, this.')).toEqual(['this', 'and', 'this']); 24 | }); 25 | 26 | it('should not remove hyphens', function() { 27 | expect(tokenizer.tokenize('billy-bob')).toEqual(['billy-bob']); 28 | }); 29 | 30 | it('should not remove apostrophes', function() { 31 | expect(tokenizer.tokenize('it\'s')).toEqual(['it\'s']); 32 | }); 33 | 34 | it('should handle cyrillic', function() { 35 | expect(tokenizer.tokenize('Есть хоть одна девушка, которую ты хочешь? Или ты устал от женщин')) 36 | .toEqual([ 37 | 'Есть', 38 | 'хоть', 39 | 'одна', 40 | 'девушка', 41 | 'которую', 42 | 'ты', 43 | 'хочешь', 44 | 'Или', 45 | 'ты', 46 | 'устал', 47 | 'от', 48 | 'женщин' 49 | ]); 50 | }); 51 | }); -------------------------------------------------------------------------------- /source/Tokenizer/StemmingTokenizer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ITokenizer } from './Tokenizer'; 4 | 5 | type StemmingFunction = (text : string) => string; 6 | 7 | /** 8 | * Stemming is the process of reducing search tokens to their root (or stem) so that searches for different forms of a 9 | * word will match. For example "search", "searching" and "searched" are all reduced to the stem "search". 10 | * 11 | *

This stemming tokenizer converts tokens (words) to their stem forms before returning them. It requires an 12 | * external stemming function to be provided; for this purpose I recommend the NPM 'porter-stemmer' library. 13 | * 14 | *

For more information see http : //tartarus.org/~martin/PorterStemmer/ 15 | */ 16 | export class StemmingTokenizer implements ITokenizer { 17 | _stemmingFunction : StemmingFunction; 18 | _tokenizer : ITokenizer; 19 | 20 | /** 21 | * Constructor. 22 | * 23 | * @param stemmingFunction Function capable of accepting a word and returning its stem. 24 | * @param decoratedIndexStrategy Index strategy to be run after all stop words have been removed. 25 | */ 26 | constructor( 27 | stemmingFunction : StemmingFunction, 28 | decoratedTokenizer : ITokenizer 29 | ) { 30 | this._stemmingFunction = stemmingFunction; 31 | this._tokenizer = decoratedTokenizer; 32 | } 33 | 34 | /** 35 | * @inheritDocs 36 | */ 37 | tokenize(text : string) : Array { 38 | return this._tokenizer 39 | .tokenize(text) 40 | .map(this._stemmingFunction); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /source/Tokenizer/StopWordsTokenizer.test.js: -------------------------------------------------------------------------------- 1 | import { SimpleTokenizer } from './SimpleTokenizer'; 2 | import { StopWordsTokenizer } from './StopWordsTokenizer'; 3 | import { StopWordsMap } from '../StopWordsMap'; 4 | 5 | describe('StopWordsTokenizer', function() { 6 | var tokenizer; 7 | 8 | beforeEach(function() { 9 | tokenizer = new StopWordsTokenizer(new SimpleTokenizer()); 10 | }); 11 | 12 | it('should handle empty values', function() { 13 | expect(tokenizer.tokenize('')).toEqual([]); 14 | expect(tokenizer.tokenize(' ')).toEqual([]); 15 | }); 16 | 17 | it('should not remove tokens that are not stop words', function() { 18 | expect(tokenizer.tokenize('software')).toEqual(['software']); 19 | }); 20 | 21 | it('should remove stop word tokens', function() { 22 | expect(tokenizer.tokenize('and testing')).toEqual(['testing']); 23 | }); 24 | 25 | it('should handle all stop word token sets', function() { 26 | expect(tokenizer.tokenize('a and the')).toEqual([]); 27 | }); 28 | 29 | it('should not remove Object.prototype properties', function() { 30 | expect(tokenizer.tokenize('constructor')).toEqual(['constructor']); 31 | expect(tokenizer.tokenize('hasOwnProperty')).toEqual(['hasOwnProperty']); 32 | expect(tokenizer.tokenize('toString')).toEqual(['toString']); 33 | expect(tokenizer.tokenize('valueOf')).toEqual(['valueOf']); 34 | }); 35 | 36 | it('should allow stop-words to be overridden', function() { 37 | StopWordsMap.the = false; 38 | expect(tokenizer.tokenize('a and the')).toEqual(['the']); 39 | StopWordsMap.the = true; 40 | }); 41 | }); -------------------------------------------------------------------------------- /benchmarks/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | beautify-benchmark@^0.2.4: 6 | version "0.2.4" 7 | resolved "https://registry.yarnpkg.com/beautify-benchmark/-/beautify-benchmark-0.2.4.tgz#3151def14c1a2e0d07ff2e476861c7ed0e1ae39b" 8 | 9 | benchmark@^2.1.3: 10 | version "2.1.3" 11 | resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.3.tgz#e10e40e4d53d0e1c9d77a834fde593994dca7f0c" 12 | dependencies: 13 | lodash "^4.17.3" 14 | platform "^1.3.3" 15 | 16 | bindings@1.2.x: 17 | version "1.2.1" 18 | resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" 19 | 20 | js-search@^1.3.5: 21 | version "1.3.5" 22 | resolved "https://registry.yarnpkg.com/js-search/-/js-search-1.3.5.tgz#b489c05aabc4b22cdb75484fe75ac49fa94feff2" 23 | 24 | lodash@^4.17.19, lodash@^4.17.3: 25 | version "4.17.19" 26 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" 27 | 28 | lunr@^0.7.2: 29 | version "0.7.2" 30 | resolved "https://registry.yarnpkg.com/lunr/-/lunr-0.7.2.tgz#79a30e932e216cba163541ee37a3607c12cd7281" 31 | 32 | microtime@^2.1.2: 33 | version "2.1.2" 34 | resolved "https://registry.yarnpkg.com/microtime/-/microtime-2.1.2.tgz#9c955d0781961ab13a1b6f9a82b080f5d7ecd83b" 35 | dependencies: 36 | bindings "1.2.x" 37 | nan "2.4.x" 38 | 39 | nan@2.4.x: 40 | version "2.4.0" 41 | resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232" 42 | 43 | platform@^1.3.3: 44 | version "1.3.3" 45 | resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.3.tgz#646c77011899870b6a0903e75e997e8e51da7461" 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-search", 3 | "version": "2.0.1", 4 | "description": "JS Search is an efficient, client-side search library for JavaScript and JSON objects", 5 | "main": "./dist/umd/js-search.js", 6 | "module": "./dist/esm/js-search.js", 7 | "exports": { 8 | ".": { 9 | "require": "./dist/umd/js-search.js", 10 | "import": "./dist/esm/js-search.js" 11 | } 12 | }, 13 | "files": [ 14 | "dist", 15 | "source" 16 | ], 17 | "devDependencies": { 18 | "@babel/core": "^7.8.7", 19 | "@babel/preset-env": "^7.8.7", 20 | "@babel/preset-flow": "^7.8.3", 21 | "babel-jest": "^25.1.0", 22 | "flow-bin": "^0.120.1", 23 | "jest": "^25.1.0", 24 | "prettier": "^1.19.1", 25 | "rimraf": "^2.5.4", 26 | "rollup": "^2.0.5", 27 | "rollup-plugin-babel": "^4.4.0", 28 | "rollup-plugin-terser": "^5.3.0" 29 | }, 30 | "scripts": { 31 | "build:flow": "echo \"// @flow\n\nexport * from '../../source'\" > dist/umd/js-search.js.flow", 32 | "build": "rimraf dist && rollup -c && yarn build:flow", 33 | "test": "flow check && jest", 34 | "tdd": "jest --watch", 35 | "prepublishOnly": "yarn build" 36 | }, 37 | "author": "Brian Vaughn (https://github.com/bvaughn/)", 38 | "license": "MIT", 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/bvaughn/js-search.git" 42 | }, 43 | "keywords": [ 44 | "search", 45 | "javascript", 46 | "js", 47 | "clientside", 48 | "client-side", 49 | "local", 50 | "query" 51 | ], 52 | "bugs": { 53 | "url": "https://github.com/bvaughn/js-search/issues" 54 | }, 55 | "homepage": "https://github.com/bvaughn/js-search", 56 | "jest": { 57 | "setupFiles": [], 58 | "roots": [ 59 | "./source" 60 | ], 61 | "testRegex": "\\.test\\.js$", 62 | "verbose": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /benchmarks/create-index.js: -------------------------------------------------------------------------------- 1 | const Benchmark = require('benchmark'); 2 | const bb = require('beautify-benchmark'); 3 | const lunr = require('lunr'); 4 | const JsSearchLatest = require('js-search'); 5 | const JsSearchLocal = require('../dist/umd/js-search'); 6 | 7 | let books; 8 | function loadBooks() { 9 | const fs = require('fs'); 10 | fs.readFile( 11 | 'books.json', 12 | 'utf8', 13 | (err, data) => { 14 | books = data; 15 | runTests(); 16 | } 17 | ); 18 | } 19 | 20 | function doSearch(Search, SearchIndex) { 21 | var search = new Search('isbn'); 22 | search.searchIndex = new SearchIndex('isbn'); 23 | search.addIndex('title'); 24 | search.addIndex('author'); 25 | search.addDocuments(books); 26 | } 27 | 28 | function runTests() { 29 | new Benchmark.Suite() 30 | .on('cycle', (event) => { 31 | console.log(String(event.target)); 32 | bb.add(event.target); 33 | }) 34 | .on('complete', () => { 35 | bb.log(); 36 | }) 37 | .add('lunr', () => { 38 | var lunrJsIndex = new lunr.Index 39 | lunrJsIndex.field('title') 40 | lunrJsIndex.field('author') 41 | lunrJsIndex.ref('isbn') 42 | for (var i = 0, length = books.length; i < length; i++) { 43 | lunrJsIndex.add(books[i]); 44 | } 45 | }) 46 | .add('js-search:latest (TF-IDF index)', () => { 47 | doSearch(JsSearchLatest.Search, JsSearchLatest.TfIdfSearchIndex); 48 | }) 49 | .add('js-search:latest (unordered index)', () => { 50 | doSearch(JsSearchLatest.Search, JsSearchLatest.UnorderedSearchIndex); 51 | }) 52 | .add('js-search:local (TF-IDF index)', () => { 53 | doSearch(JsSearchLocal.Search, JsSearchLocal.TfIdfSearchIndex); 54 | }) 55 | .add('js-search:local (unordered index)', () => { 56 | doSearch(JsSearchLocal.Search, JsSearchLocal.UnorderedSearchIndex); 57 | }) 58 | .run({ 'async': true }); 59 | } 60 | 61 | 62 | loadBooks(); -------------------------------------------------------------------------------- /source/SearchIndex/UnorderedSearchIndex.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ISearchIndex } from './SearchIndex'; 4 | 5 | /** 6 | * Search index capable of returning results matching a set of tokens but without any meaningful rank or order. 7 | */ 8 | export class UnorderedSearchIndex implements ISearchIndex { 9 | _tokenToUidToDocumentMap : {[token : string] : {[uid : string] : any}}; 10 | 11 | constructor() { 12 | this._tokenToUidToDocumentMap = {}; 13 | } 14 | 15 | /** 16 | * @inheritDocs 17 | */ 18 | indexDocument(token : string, uid : string, doc : Object) : void { 19 | if (typeof this._tokenToUidToDocumentMap[token] !== 'object') { 20 | this._tokenToUidToDocumentMap[token] = {}; 21 | } 22 | 23 | this._tokenToUidToDocumentMap[token][uid] = doc; 24 | } 25 | 26 | /** 27 | * @inheritDocs 28 | */ 29 | search(tokens : Array, corpus : Array) : Array { 30 | var intersectingDocumentMap = {}; 31 | 32 | var tokenToUidToDocumentMap = this._tokenToUidToDocumentMap; 33 | 34 | for (var i = 0, numTokens = tokens.length; i < numTokens; i++) { 35 | var token = tokens[i]; 36 | var documentMap = tokenToUidToDocumentMap[token]; 37 | 38 | // Short circuit if no matches were found for any given token. 39 | if (!documentMap) { 40 | return []; 41 | } 42 | 43 | if (i === 0) { 44 | var keys = Object.keys(documentMap); 45 | 46 | for (var j = 0, numKeys = keys.length; j < numKeys; j++) { 47 | var uid = keys[j]; 48 | 49 | intersectingDocumentMap[uid] = documentMap[uid]; 50 | } 51 | } else { 52 | var keys = Object.keys(intersectingDocumentMap); 53 | 54 | for (var j = 0, numKeys = keys.length; j < numKeys; j++) { 55 | var uid = keys[j]; 56 | 57 | if (typeof documentMap[uid] !== 'object') { 58 | delete intersectingDocumentMap[uid]; 59 | } 60 | } 61 | } 62 | } 63 | 64 | var keys = Object.keys(intersectingDocumentMap); 65 | var documents = []; 66 | 67 | for (var i = 0, numKeys = keys.length; i < numKeys; i++) { 68 | var uid = keys[i]; 69 | 70 | documents.push(intersectingDocumentMap[uid]); 71 | } 72 | 73 | return documents; 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /benchmarks/search.js: -------------------------------------------------------------------------------- 1 | const Benchmark = require('benchmark'); 2 | const bb = require('beautify-benchmark'); 3 | const lunr = require('lunr'); 4 | const JsSearchLatest = require('js-search'); 5 | const JsSearchLocal = require('../dist/umd/js-search'); 6 | 7 | let books; 8 | function loadBooks() { 9 | const fs = require('fs'); 10 | fs.readFile( 11 | 'books.json', 12 | 'utf8', 13 | (err, data) => { 14 | books = JSON.parse(data); 15 | setupTest(); 16 | } 17 | ); 18 | } 19 | 20 | var lunrJsIndex; 21 | var searchLatest; 22 | var searchLatestTfIdf; 23 | var searchLocal; 24 | var searchLocalTfIdf; 25 | var searchTerms = ['letter', 'world', 'wife', 'love', 'foobar']; 26 | var searchTermsLength = searchTerms.length; 27 | 28 | function setupTest() { 29 | lunrJsIndex = new lunr.Index(); 30 | lunrJsIndex.field('title'); 31 | lunrJsIndex.field('author'); 32 | lunrJsIndex.ref('isbn'); 33 | for (var i = 0, length = books.length; i < length; i++) { 34 | lunrJsIndex.add(books[i]); 35 | } 36 | 37 | searchLatest = buildIndex(JsSearchLatest.Search, JsSearchLatest.UnorderedSearchIndex); 38 | searchLatestTfIdf = buildIndex(JsSearchLatest.Search, JsSearchLatest.TfIdfSearchIndex); 39 | searchLocal = buildIndex(JsSearchLocal.Search, JsSearchLocal.UnorderedSearchIndex); 40 | searchLocalTfIdf = buildIndex(JsSearchLocal.Search, JsSearchLocal.TfIdfSearchIndex); 41 | 42 | runTests(); 43 | } 44 | 45 | function buildIndex(Search, SearchIndex) { 46 | var search = new Search('isbn'); 47 | search.searchIndex = new SearchIndex('isbn'); 48 | search.addIndex('title'); 49 | search.addIndex('author'); 50 | search.addDocuments(books); 51 | 52 | return search; 53 | } 54 | 55 | function doSearch(search) { 56 | for (var i = 0, length = searchTermsLength; i < length; i++) { 57 | search.search(searchTerms[i]); 58 | } 59 | } 60 | 61 | function runTests() { 62 | new Benchmark.Suite() 63 | .on('cycle', (event) => { 64 | console.log(String(event.target)); 65 | bb.add(event.target); 66 | }) 67 | .on('complete', () => { 68 | bb.log(); 69 | }) 70 | .add('lunr', () => { 71 | doSearch(lunrJsIndex); 72 | }) 73 | .add('js-search:latest (TF-IDF index)', () => { 74 | doSearch(searchLatestTfIdf); 75 | }) 76 | .add('js-search:latest (unordered index)', () => { 77 | doSearch(searchLatest); 78 | }) 79 | .add('js-search:local (TF-IDF index)', () => { 80 | doSearch(searchLocalTfIdf); 81 | }) 82 | .add('js-search:local (unordered index)', () => { 83 | doSearch(searchLocal); 84 | }) 85 | .run({ 'async': true }); 86 | } 87 | 88 | loadBooks(); 89 | -------------------------------------------------------------------------------- /source/StopWordsMap.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Stop words list copied from Lunr JS. 5 | */ 6 | export var StopWordsMap : Object = { 7 | a: true, 8 | able: true, 9 | about: true, 10 | across: true, 11 | after: true, 12 | all: true, 13 | almost: true, 14 | also: true, 15 | am: true, 16 | among: true, 17 | an: true, 18 | and: true, 19 | any: true, 20 | are: true, 21 | as: true, 22 | at: true, 23 | be: true, 24 | because: true, 25 | been: true, 26 | but: true, 27 | by: true, 28 | can: true, 29 | cannot: true, 30 | could: true, 31 | dear: true, 32 | did: true, 33 | 'do': true, 34 | does: true, 35 | either: true, 36 | 'else': true, 37 | ever: true, 38 | every: true, 39 | 'for': true, 40 | from: true, 41 | 'get': true, 42 | got: true, 43 | had: true, 44 | has: true, 45 | have: true, 46 | he: true, 47 | her: true, 48 | hers: true, 49 | him: true, 50 | his: true, 51 | how: true, 52 | however: true, 53 | i: true, 54 | 'if': true, 55 | 'in': true, 56 | into: true, 57 | is: true, 58 | it: true, 59 | its: true, 60 | just: true, 61 | least: true, 62 | let: true, 63 | like: true, 64 | likely: true, 65 | may: true, 66 | me: true, 67 | might: true, 68 | most: true, 69 | must: true, 70 | my: true, 71 | neither: true, 72 | no: true, 73 | nor: true, 74 | not: true, 75 | of: true, 76 | off: true, 77 | often: true, 78 | on: true, 79 | only: true, 80 | or: true, 81 | other: true, 82 | our: true, 83 | own: true, 84 | rather: true, 85 | said: true, 86 | say: true, 87 | says: true, 88 | she: true, 89 | should: true, 90 | since: true, 91 | so: true, 92 | some: true, 93 | than: true, 94 | that: true, 95 | the: true, 96 | their: true, 97 | them: true, 98 | then: true, 99 | there: true, 100 | these: true, 101 | they: true, 102 | 'this': true, 103 | tis: true, 104 | to: true, 105 | too: true, 106 | twas: true, 107 | us: true, 108 | wants: true, 109 | was: true, 110 | we: true, 111 | were: true, 112 | what: true, 113 | when: true, 114 | where: true, 115 | which: true, 116 | 'while': true, 117 | who: true, 118 | whom: true, 119 | why: true, 120 | will: true, 121 | 'with': true, 122 | would: true, 123 | yet: true, 124 | you: true, 125 | your: true 126 | }; 127 | 128 | // Prevent false positives for inherited properties 129 | StopWordsMap.constructor = false; 130 | StopWordsMap.hasOwnProperty = false; 131 | StopWordsMap.isPrototypeOf = false; 132 | StopWordsMap.propertyIsEnumerable = false; 133 | StopWordsMap.toLocaleString = false; 134 | StopWordsMap.toString = false; 135 | StopWordsMap.valueOf = false; 136 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.1 4 | README update. (No code changes.) 5 | 6 | ## 2.0.0 7 | 8 | Added es modules support for bundlers via "module" field and for node via "exports" field. 9 | Commonjs output is no longer provided. Entry point is UMD now. 10 | UMD/ESM are bundled with rollup which reduced minified bundle size twice from 17432 to 7759 bytes! 11 | Flow types are distributed with sources. 12 | 13 | ## 1.4.3 14 | Don't inherit from the default Object for the token dictionary. ([davidlukerice](https://github.com/davidlukerice) - [#73](https://github.com/bvaughn/js-search/pull/73)) 15 | 16 | ## 1.4.2 17 | Throw an error if `Search` is instantiated without the required `uidFieldName` constructor parameter. 18 | 19 | ## 1.4.1 20 | - 21 | 22 | ## 1.4.0 23 | Search uid field can now be an array (for nested/deep keys). 24 | 25 | ## 1.3.7 26 | Fixed `package.json` to include correct files. 27 | 28 | ## 1.3.6 29 | Performance tuning and removal of eager deopts. 30 | 31 | Behind the scenes, this release also includes a rewrite from TypeScript to Flowtype. 32 | The external API should not be impacted by this rewrite however. 33 | 34 | ## 1.3.5 35 | Fixed (hopefully) previous broken build. 36 | 37 | ## 1.3.4 38 | Simple tokenizer now supports cyrillic. ([De-Luxis](https://github.com/De-Luxis) - [#21](https://github.com/bvaughn/js-search/pull/21)) 39 | 40 | ## 1.3.3 41 | Fixed a bug in `TfIdfSearchIndex` that caused errors when indexing certain reserved keywords (eg "constructor"). 42 | 43 | ## 1.3.2 44 | Fixed tokenizer bug affecting IE <= 10 that caused prefix and substring token strategies incorrectly index terms. 45 | 46 | ## 1.3.1 47 | Replaced `array.push.call` with `array.concat` in `addDocuments`. 48 | This avoids potential stack overflow for large documents arrays. 49 | 50 | ## 1.3.0 51 | `Search.addIndex` supports `Array` parameter for nested values. 52 | `Search` indexing supports non-string values (eg numbers). 53 | Special thanks to @konradjurk for this release. 54 | 55 | ## 1.2.2 56 | Small tweak to Node export check to avoid `module is not defined` error for browser-based users. 57 | 58 | ## 1.2.1 59 | Modified export to better support Node environment (thanks to @scommisso). 60 | 61 | ## 1.2.0 62 | Added `ISearchIndex` interface in order to support TF-IDF (enabled by default). 63 | Removed `IPruningStrategy`; it didn't seem like it added sufficient value to offset performance costs. 64 | 65 | ## 1.1.1 66 | Udpated stop-words list to avoid filtering `Object.prototype` properties. 67 | 68 | ## 1.1.0 69 | Refactored stemming and stop-word support to be based on `ITokenizer` decorators for better accuracy. 70 | Updated README examples with more info. 71 | 72 | ## 1.0.2 73 | Added `JsSearch` module wrapper around library and renamed `JsSearch` class to `Search`. 74 | Added stemming support by way of the new `StemmingSanitizerDecorator` class. 75 | 76 | ## 1.0.1 77 | Renamed `WhitespaceTokenizer` to `SimpleTokenizer` and added better support for punctuation. 78 | Added `StopWordsIndexStrategyDecorator` to support stop words filtering. 79 | 80 | ## 1.0.0 81 | Initial release! 82 | -------------------------------------------------------------------------------- /source/TokenHighlighter.test.js: -------------------------------------------------------------------------------- 1 | import { TokenHighlighter } from './TokenHighlighter'; 2 | 3 | describe('TokenHighlighter', function() { 4 | var tag, tokenHighlighter; 5 | 6 | beforeEach(function() { 7 | tokenHighlighter = new TokenHighlighter(); 8 | }); 9 | 10 | it('should handle empty strings', function() { 11 | var text = ''; 12 | expect(tokenHighlighter.highlight(text, [])).toEqual(text); 13 | }); 14 | 15 | it('should not highlight strings without matches', function() { 16 | var tokens = ['foo']; 17 | var text = 'bar baz'; 18 | expect(tokenHighlighter.highlight(text, tokens)).toEqual(text); 19 | }); 20 | 21 | it('should highlight tokens that equal the full string', function() { 22 | var tokens = ['foo']; 23 | var text = 'foo'; 24 | expect(tokenHighlighter.highlight(text, tokens)).toEqual(tokenHighlighter._wrapText(text)); 25 | }); 26 | 27 | it('should not highlight words ending with tokens', function() { 28 | var tokens = ['bar']; 29 | var text = 'foobar'; 30 | expect(tokenHighlighter.highlight(text, tokens)).toEqual(text); 31 | }); 32 | 33 | it('should highlight multiple matches for multiple tokens', function() { 34 | var tokens = ['bar', 'baz']; 35 | var text = 'foo bar baz foo'; 36 | var expectedText = 'foo ' + tokenHighlighter._wrapText('bar') + ' ' + tokenHighlighter._wrapText('baz') + ' foo'; 37 | expect(tokenHighlighter.highlight(text, tokens)).toEqual(expectedText); 38 | }); 39 | 40 | it('should highlight the last word in the text', function() { 41 | var tokens = ['bar']; 42 | var text = 'foo bar'; 43 | var expectedText = 'foo ' + tokenHighlighter._wrapText('bar'); 44 | expect(tokenHighlighter.highlight(text, tokens)).toEqual(expectedText); 45 | }); 46 | 47 | it('should highlight the first word in the text', function() { 48 | var tokens = ['foo']; 49 | var text = 'foo bar'; 50 | var expectedText = tokenHighlighter._wrapText('foo') + ' bar'; 51 | expect(tokenHighlighter.highlight(text, tokens)).toEqual(expectedText); 52 | }); 53 | 54 | it('should highlight tokens within tokens', function() { 55 | var tokens = ['foo', 'foobar']; 56 | var text = 'bar foobar baz'; 57 | var expectedText = 'bar ' + tokenHighlighter._wrapText(tokenHighlighter._wrapText('foo') + 'bar') + ' baz'; 58 | expect(tokenHighlighter.highlight(text, tokens)).toEqual(expectedText); 59 | }); 60 | 61 | it('should highlight using sanitized text', function() { 62 | var tokens = ['foo', 'BAR']; 63 | var text = 'Foo bar baz'; 64 | var expectedText = tokenHighlighter._wrapText('Foo') + ' ' + tokenHighlighter._wrapText('bar') + ' baz'; 65 | expect(tokenHighlighter.highlight(text, tokens)).toEqual(expectedText); 66 | }); 67 | 68 | it('should highlight the correct words regardless of leading or trailing spaces', function() { 69 | var tokens = ['foo', 'baz']; 70 | var text = ' foo bar baz '; 71 | var expectedText = ' ' + tokenHighlighter._wrapText('foo') + ' bar ' + tokenHighlighter._wrapText('baz') + ' '; 72 | expect(tokenHighlighter.highlight(text, tokens)).toEqual(expectedText); 73 | }); 74 | }); -------------------------------------------------------------------------------- /source/TokenHighlighter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PrefixIndexStrategy } from './IndexStrategy/index'; 4 | import { LowerCaseSanitizer } from './Sanitizer/index'; 5 | 6 | import type { IIndexStrategy } from './IndexStrategy/index'; 7 | import type { ISanitizer } from './Sanitizer/index'; 8 | 9 | /** 10 | * This utility highlights the occurrences of tokens within a string of text. It can be used to give visual indicators 11 | * of match criteria within searchable fields. 12 | * 13 | *

For performance purposes this highlighter only works with full-word or prefix token indexes. 14 | */ 15 | export class TokenHighlighter { 16 | _indexStrategy : IIndexStrategy; 17 | _sanitizer : ISanitizer 18 | _wrapperTagName : string; 19 | 20 | /** 21 | * Constructor. 22 | * 23 | * @param opt_indexStrategy Index strategy used by Search 24 | * @param opt_sanitizer Sanitizer used by Search 25 | * @param opt_wrapperTagName Optional wrapper tag name; defaults to 'mark' (e.g. ) 26 | */ 27 | constructor(opt_indexStrategy : IIndexStrategy, opt_sanitizer : ISanitizer, opt_wrapperTagName:string) { 28 | this._indexStrategy = opt_indexStrategy || new PrefixIndexStrategy(); 29 | this._sanitizer = opt_sanitizer || new LowerCaseSanitizer(); 30 | this._wrapperTagName = opt_wrapperTagName || 'mark'; 31 | } 32 | 33 | /** 34 | * Highlights token occurrences within a string by wrapping them with a DOM element. 35 | * 36 | * @param text e.g. "john wayne" 37 | * @param tokens e.g. ["wa"] 38 | * @returns {string} e.g. "john wayne" 39 | */ 40 | highlight(text : string, tokens : Array) { 41 | var tagsLength : number = this._wrapText('').length; 42 | 43 | var tokenDictionary = ((Object.create(null): any): Object); 44 | 45 | // Create a token map for easier lookup below. 46 | for (var i = 0, numTokens = tokens.length; i < numTokens; i++) { 47 | var token : string = this._sanitizer.sanitize(tokens[i]); 48 | var expandedTokens : Array = this._indexStrategy.expandToken(token); 49 | 50 | for (var j = 0, numExpandedTokens = expandedTokens.length; j < numExpandedTokens; j++) { 51 | var expandedToken : string = expandedTokens[j]; 52 | 53 | if (!tokenDictionary[expandedToken]) { 54 | tokenDictionary[expandedToken] = [token]; 55 | } else { 56 | tokenDictionary[expandedToken].push(token); 57 | } 58 | } 59 | } 60 | 61 | // Track actualCurrentWord and sanitizedCurrentWord separately in case we encounter nested tags. 62 | var actualCurrentWord : string = ''; 63 | var sanitizedCurrentWord : string = ''; 64 | var currentWordStartIndex : number = 0; 65 | 66 | // Note this assumes either prefix or full word matching. 67 | for (var i = 0, textLength = text.length; i < textLength; i++) { 68 | var character : string = text.charAt(i); 69 | 70 | if (character === ' ') { 71 | actualCurrentWord = ''; 72 | sanitizedCurrentWord = ''; 73 | currentWordStartIndex = i + 1; 74 | } else { 75 | actualCurrentWord += character; 76 | sanitizedCurrentWord += this._sanitizer.sanitize(character); 77 | } 78 | 79 | if (tokenDictionary[sanitizedCurrentWord] && 80 | tokenDictionary[sanitizedCurrentWord].indexOf(sanitizedCurrentWord) >= 0) { 81 | 82 | actualCurrentWord = this._wrapText(actualCurrentWord); 83 | text = text.substring(0, currentWordStartIndex) + actualCurrentWord + text.substring(i + 1); 84 | 85 | i += tagsLength; 86 | textLength += tagsLength; 87 | } 88 | } 89 | 90 | return text; 91 | } 92 | 93 | /** 94 | * @param text to wrap 95 | * @returns Text wrapped by wrapper tag (e.g. "foo" becomes "foo") 96 | * @private 97 | */ 98 | _wrapText(text : string) : string { 99 | const tagName = this._wrapperTagName; 100 | return `<${tagName}>${text}`; 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /benchmarks/regression-test.js: -------------------------------------------------------------------------------- 1 | var Benchmark = require('benchmark'); 2 | var bb = require('beautify-benchmark'); 3 | var fs = require('fs'); 4 | 5 | var versions = [ 6 | { 7 | label: 'latest', 8 | module: require('js-search') 9 | }, 10 | { 11 | label: 'local', 12 | module: require('../dist/umd/js-search') 13 | } 14 | ] 15 | 16 | fs.readFile('books.json', 'utf8', 17 | (err, data) => setupBenchmarks(JSON.parse(data).books) 18 | ); 19 | 20 | var filter = process.argv.length === 3 21 | ? new RegExp(process.argv[2]) 22 | : null; 23 | 24 | var benchmarks = []; 25 | 26 | function setupBenchmarks(corpus) { 27 | // Index strategies 28 | initBenchmark({ 29 | corpus, 30 | indexStrategy: 'PrefixIndexStrategy' 31 | }); 32 | initBenchmark({ 33 | corpus, 34 | indexStrategy: 'AllSubstringsIndexStrategy' 35 | }); 36 | initBenchmark({ 37 | corpus, 38 | indexStrategy: 'PrefixIndexStrategy' 39 | }); 40 | 41 | // Search indices 42 | initBenchmark({ 43 | corpus, 44 | searchIndex: 'TfIdfSearchIndex' 45 | }); 46 | initBenchmark({ 47 | corpus, 48 | searchIndex: 'UnorderedSearchIndex' 49 | }); 50 | 51 | // Tokenizers 52 | initBenchmark({ 53 | corpus, 54 | tokenizer: 'SimpleTokenizer' 55 | }); 56 | initBenchmark({ 57 | corpus, 58 | tokenizer: 'StemmingTokenizer' 59 | }); 60 | initBenchmark({ 61 | corpus, 62 | tokenizer: 'StopWordsTokenizer' 63 | }); 64 | 65 | runNextTest(); 66 | } 67 | 68 | function identity(text) { 69 | return text; 70 | } 71 | 72 | function createTokenizer(module, tokenizer) { 73 | switch (tokenizer) { 74 | case 'SimpleTokenizer': 75 | return new module.SimpleTokenizer(); 76 | case 'StemmingTokenizer': 77 | return new module.StemmingTokenizer( 78 | identity, 79 | new module.SimpleTokenizer() 80 | ); 81 | case 'StopWordsTokenizer': 82 | return new module.StopWordsTokenizer( 83 | new module.SimpleTokenizer() 84 | ); 85 | } 86 | } 87 | 88 | function createBenchmark() { 89 | return new Benchmark.Suite() 90 | .on('cycle', (event) => { 91 | bb.add(event.target); 92 | }) 93 | .on('complete', () => { 94 | bb.log(); 95 | 96 | runNextTest(); 97 | }); 98 | } 99 | 100 | function runNextTest() { 101 | if (benchmarks.length) { 102 | benchmarks.pop().run({ 'async': true }); 103 | } 104 | } 105 | 106 | function initBenchmark({ 107 | corpus, 108 | indexStrategy = 'PrefixIndexStrategy', 109 | searchIndex = 'UnorderedSearchIndex', 110 | tokenizer = 'SimpleTokenizer' 111 | }) { 112 | initBenchmarkForCreateIndex({ 113 | corpus, 114 | indexStrategy, 115 | searchIndex, 116 | tokenizer 117 | }); 118 | initBenchmarkForSearch({ 119 | corpus, 120 | indexStrategy, 121 | searchIndex, 122 | tokenizer 123 | }); 124 | } 125 | 126 | function initBenchmarkForCreateIndex({ 127 | corpus, 128 | indexStrategy, 129 | searchIndex, 130 | tokenizer 131 | }) { 132 | var label = `index\t${indexStrategy}\t${searchIndex}\t${tokenizer}`; 133 | 134 | if (filter && !label.match(filter)) { 135 | return; 136 | } 137 | 138 | var benchmark = createBenchmark(); 139 | 140 | versions.forEach(version => { 141 | var IndexStrategy = version.module[indexStrategy]; 142 | var Search = version.module.Search; 143 | var SearchIndex = version.module[searchIndex]; 144 | 145 | benchmark.add(`[${version.label}]\t${label}`, () => { 146 | var search = new Search('isbn'); 147 | search.indexStrategy = new IndexStrategy(); 148 | search.searchIndex = new SearchIndex('isbn'); 149 | search.tokenizer = createTokenizer(version.module, tokenizer); 150 | search.addIndex('title'); 151 | search.addIndex('author'); 152 | search.addDocuments(corpus); 153 | }); 154 | }); 155 | 156 | benchmarks.push(benchmark); 157 | } 158 | 159 | function initBenchmarkForSearch({ 160 | corpus, 161 | indexStrategy, 162 | searchIndex, 163 | tokenizer 164 | }) { 165 | var label = `search\t${indexStrategy}\t${searchIndex}\t${tokenizer}`; 166 | 167 | if (filter && !label.match(filter)) { 168 | return; 169 | } 170 | 171 | var searchTerms = ['letter', 'world', 'wife', 'love', 'foobar']; 172 | var searchTermsLength = searchTerms.length; 173 | 174 | var benchmark = createBenchmark(); 175 | 176 | versions.forEach(version => { 177 | var IndexStrategy = version.module[indexStrategy]; 178 | var Search = version.module.Search; 179 | var SearchIndex = version.module[searchIndex]; 180 | 181 | var search = new Search('isbn'); 182 | search.indexStrategy = new IndexStrategy(); 183 | search.searchIndex = new SearchIndex('isbn'); 184 | search.tokenizer = createTokenizer(version.module, tokenizer); 185 | search.addIndex('title'); 186 | search.addIndex('author'); 187 | search.addDocuments(corpus); 188 | 189 | benchmark.add(`[${version.label}]\t${label}`, () => { 190 | for (var i = 0, length = searchTermsLength; i < length; i++) { 191 | search.search(searchTerms[i]); 192 | } 193 | }); 194 | }); 195 | 196 | benchmarks.push(benchmark); 197 | } 198 | -------------------------------------------------------------------------------- /source/Search.test.js: -------------------------------------------------------------------------------- 1 | import { Search } from './Search'; 2 | 3 | describe('Search', function() { 4 | var documentBar, documentBaz, documentFoo, nestedDocumentFoo, search; 5 | 6 | var validateSearchResults = function(results, expectedDocuments) { 7 | expect(results.length).toBe(expectedDocuments.length); 8 | expectedDocuments.forEach(function(document) { 9 | expect(results).toContain(document); 10 | }); 11 | }; 12 | 13 | beforeEach(function() { 14 | search = new Search('uid'); 15 | 16 | documentBar = { 17 | uid: 'bar', 18 | title: 'Bar', 19 | description: 'This is a document about bar', 20 | aNumber: 0, 21 | aBoolean: false 22 | }; 23 | documentBaz = { 24 | uid: 'baz', 25 | title: 'BAZ', 26 | description: 'All about baz', 27 | array: ['test', true, 456] 28 | }; 29 | documentFoo = { 30 | uid: 'foo', 31 | title: 'foo', 32 | description: 'Is kung foo the same as kung fu?', 33 | aNumber: 167543, 34 | aBoolean: true, 35 | array: [123, 'test', 'foo'] 36 | }; 37 | nestedDocumentFoo = { 38 | uid: 'foo', 39 | title: 'foo', 40 | description: 'Is kung foo the same as kung fu?', 41 | nested: { 42 | title: 'nested foo' 43 | } 44 | } 45 | }); 46 | 47 | it('should throw an error if instantiated without the :uidFieldName parameter', function() { 48 | expect(function() { 49 | new Search(); 50 | }).toThrow(); 51 | }); 52 | 53 | it('should index a new document on all searchable fields', function() { 54 | search.addIndex('title'); 55 | spyOn(search._indexStrategy, 'expandToken').and.returnValue([]); 56 | search.addDocument(documentBar); 57 | expect(search._indexStrategy.expandToken).toHaveBeenCalledWith('bar'); 58 | }); 59 | 60 | it('should re-index existing documents if a new searchable field is added', function() { 61 | search.addDocument(documentBar); 62 | spyOn(search._indexStrategy, 'expandToken').and.returnValue([]); 63 | search.addIndex('title'); 64 | expect(search._indexStrategy.expandToken).toHaveBeenCalledWith('bar'); 65 | 66 | }); 67 | 68 | it('should find matches for all searchable fields', function() { 69 | search.addIndex('title'); 70 | search.addIndex('description'); 71 | search.addDocument(documentFoo); 72 | // Search title text 73 | validateSearchResults(search.search('foo'), [documentFoo]); 74 | // Search description text 75 | validateSearchResults(search.search('kung'), [documentFoo]); 76 | }); 77 | 78 | it('should find no matches if none exist', function() { 79 | search.addIndex('title'); 80 | search.addDocument(documentFoo); 81 | validateSearchResults(search.search('xyz'), []); 82 | }); 83 | 84 | it('should find no matches if one token is empty', function() { 85 | search.addIndex('title'); 86 | search.addDocument(documentFoo); 87 | validateSearchResults(search.search('foo xyz'), []); 88 | }); 89 | 90 | it('should index and find non-string values if they can be converted to strings', function() { 91 | search.addIndex('aBoolean'); 92 | search.addIndex('aNumber'); 93 | search.addDocument(documentBar); 94 | search.addDocument(documentFoo); 95 | 96 | validateSearchResults(search.search('167'), [documentFoo]); 97 | validateSearchResults(search.search('true'), [documentFoo]); 98 | validateSearchResults(search.search('0'), [documentBar]); 99 | validateSearchResults(search.search('false'), [documentBar]); 100 | }); 101 | 102 | it('should stringified arrays', function() { 103 | search.addIndex('array'); 104 | search.addDocuments([documentFoo, documentBaz]); 105 | 106 | validateSearchResults(search.search('test'), [documentFoo, documentBaz]); 107 | validateSearchResults(search.search('true'), [documentBaz]); 108 | validateSearchResults(search.search('456'), [documentBaz]); 109 | }); 110 | 111 | it('should index nested document properties', function() { 112 | search.addIndex(['nested', 'title']); 113 | search.addDocument(nestedDocumentFoo); 114 | 115 | validateSearchResults(search.search('nested foo'), [nestedDocumentFoo]); 116 | }); 117 | 118 | it('should gracefully handle broken property path', function() { 119 | search.addIndex(['nested', 'title', 'not', 'existing']); 120 | search.addDocument(nestedDocumentFoo); 121 | 122 | validateSearchResults(search.search('nested foo'), []); 123 | }); 124 | 125 | it('should support nested uid paths', function() { 126 | const melissaSmith = { 127 | name: 'Melissa Smith', 128 | email: 'melissa.allen@example.com', 129 | login: { 130 | userId: 2562, 131 | username: 'heavycat937' 132 | } 133 | }; 134 | const johnSmith = { 135 | name: 'John Smith', 136 | email: 'john.allen@example.com', 137 | login: { 138 | userId: 54213, 139 | username: 'jhon123' 140 | } 141 | }; 142 | 143 | var search = new Search(['login', 'userId']); 144 | search.addIndex('title'); 145 | search.addIndex(['login', 'username']); 146 | search.addIndex('name'); 147 | search.addIndex('email'); 148 | search.addDocuments([melissaSmith, johnSmith]); 149 | 150 | validateSearchResults(search.search('Smith'), [melissaSmith, johnSmith]); 151 | validateSearchResults(search.search('allen'), [melissaSmith, johnSmith]); 152 | validateSearchResults(search.search('heavycat937'), [melissaSmith]); 153 | validateSearchResults(search.search('jhon123'), [johnSmith]); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /source/SearchIndex/TfIdfSearchIndex.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import getNestedFieldValue from '../getNestedFieldValue'; 3 | 4 | import type { ISearchIndex } from './SearchIndex'; 5 | 6 | type ITfIdfTokenMap = { 7 | [token : string] : ITfIdfTokenMetadata; 8 | }; 9 | 10 | type ITfIdfUidMap = { 11 | [uid : string] : ITfIdfUidMetadata; 12 | }; 13 | 14 | type ITfIdfTokenMetadata = { 15 | $numDocumentOccurrences : number; 16 | $totalNumOccurrences : number; 17 | $uidMap : ITfIdfUidMap; 18 | }; 19 | 20 | type ITfIdfUidMetadata = { 21 | $document : Object; 22 | $numTokenOccurrences : number; 23 | }; 24 | 25 | /** 26 | * Search index capable of returning results matching a set of tokens and ranked according to TF-IDF. 27 | */ 28 | export class TfIdfSearchIndex implements ISearchIndex { 29 | _uidFieldName : string | Array; 30 | _tokenToIdfCache : {[token : string] : number}; 31 | _tokenMap : ITfIdfTokenMap; 32 | 33 | constructor(uidFieldName : string | Array) { 34 | this._uidFieldName = uidFieldName; 35 | this._tokenToIdfCache = {}; 36 | this._tokenMap = {}; 37 | } 38 | 39 | /** 40 | * @inheritDocs 41 | */ 42 | indexDocument(token : string, uid : string, doc : Object) : void { 43 | this._tokenToIdfCache = {}; // New index invalidates previous IDF caches 44 | 45 | var tokenMap = this._tokenMap; 46 | var tokenDatum; 47 | 48 | if (typeof tokenMap[token] !== 'object') { 49 | tokenMap[token] = tokenDatum = { 50 | $numDocumentOccurrences: 0, 51 | $totalNumOccurrences: 1, 52 | $uidMap: {}, 53 | }; 54 | } else { 55 | tokenDatum = tokenMap[token]; 56 | tokenDatum.$totalNumOccurrences++; 57 | } 58 | 59 | var uidMap = tokenDatum.$uidMap; 60 | 61 | if (typeof uidMap[uid] !== 'object') { 62 | tokenDatum.$numDocumentOccurrences++; 63 | uidMap[uid] = { 64 | $document: doc, 65 | $numTokenOccurrences: 1 66 | }; 67 | } else { 68 | uidMap[uid].$numTokenOccurrences++; 69 | } 70 | } 71 | 72 | /** 73 | * @inheritDocs 74 | */ 75 | search(tokens : Array, corpus : Array) : Array { 76 | var uidToDocumentMap : {[uid : string] : Object} = {}; 77 | 78 | for (var i = 0, numTokens = tokens.length; i < numTokens; i++) { 79 | var token = tokens[i]; 80 | var tokenMetadata = this._tokenMap[token]; 81 | 82 | // Short circuit if no matches were found for any given token. 83 | if (!tokenMetadata) { 84 | return []; 85 | } 86 | 87 | if (i === 0) { 88 | var keys = Object.keys(tokenMetadata.$uidMap); 89 | for (var j = 0, numKeys = keys.length; j < numKeys; j++) { 90 | var uid = keys[j]; 91 | 92 | uidToDocumentMap[uid] = tokenMetadata.$uidMap[uid].$document; 93 | } 94 | } else { 95 | var keys = Object.keys(uidToDocumentMap); 96 | for (var j = 0, numKeys = keys.length; j < numKeys; j++) { 97 | var uid = keys[j]; 98 | 99 | if (typeof tokenMetadata.$uidMap[uid] !== 'object') { 100 | delete uidToDocumentMap[uid]; 101 | } 102 | } 103 | } 104 | } 105 | 106 | var documents = []; 107 | 108 | for (var uid in uidToDocumentMap) { 109 | documents.push(uidToDocumentMap[uid]); 110 | } 111 | 112 | var calculateTfIdf = this._createCalculateTfIdf(); 113 | 114 | // Return documents sorted by TF-IDF 115 | return documents.sort((documentA, documentB) => 116 | calculateTfIdf(tokens, documentB, corpus) - 117 | calculateTfIdf(tokens, documentA, corpus) 118 | ); 119 | } 120 | 121 | _createCalculateIdf () : Function { 122 | var tokenMap = this._tokenMap; 123 | var tokenToIdfCache = this._tokenToIdfCache; 124 | 125 | return function calculateIdf(token : string, documents : Array) : number { 126 | if (!tokenToIdfCache[token]) { 127 | var numDocumentsWithToken:number = typeof tokenMap[token] !== 'undefined' 128 | ? tokenMap[token].$numDocumentOccurrences 129 | : 0; 130 | 131 | tokenToIdfCache[token] = 1 + Math.log(documents.length / (1 + numDocumentsWithToken)); 132 | } 133 | 134 | return tokenToIdfCache[token]; 135 | } 136 | } 137 | 138 | _createCalculateTfIdf () : Function { 139 | var tokenMap = this._tokenMap; 140 | var uidFieldName = this._uidFieldName; 141 | var calculateIdf = this._createCalculateIdf(); 142 | 143 | return function calculateTfIdf(tokens : Array, document : Object, documents : Array) : number { 144 | var score:number = 0; 145 | 146 | for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { 147 | var token:string = tokens[i]; 148 | 149 | var inverseDocumentFrequency:number = calculateIdf(token, documents); 150 | 151 | if (inverseDocumentFrequency === Infinity) { 152 | inverseDocumentFrequency = 0; 153 | } 154 | 155 | var uid:any; 156 | if (uidFieldName instanceof Array) { 157 | uid = document && getNestedFieldValue(document, uidFieldName); 158 | } else { 159 | uid = document && document[uidFieldName]; 160 | } 161 | 162 | var termFrequency:number = 163 | typeof tokenMap[token] !== 'undefined' && 164 | typeof tokenMap[token].$uidMap[uid] !== 'undefined' 165 | ? tokenMap[token].$uidMap[uid].$numTokenOccurrences 166 | : 0; 167 | 168 | score += termFrequency * inverseDocumentFrequency; 169 | } 170 | 171 | return score; 172 | } 173 | } 174 | }; 175 | -------------------------------------------------------------------------------- /source/SearchIndex/TfIdfSearchIndex.test.js: -------------------------------------------------------------------------------- 1 | import { Search } from '../Search'; 2 | import { TfIdfSearchIndex } from './TfIdfSearchIndex'; 3 | 4 | describe('Search', function() { 5 | var documents, search, uid; 6 | 7 | var addDocument = function(title) { 8 | var document = { 9 | uid: ++uid, 10 | title: title 11 | }; 12 | 13 | documents.push(document); 14 | search.addDocument(document); 15 | 16 | return document; 17 | }; 18 | 19 | beforeEach(function() { 20 | documents = []; 21 | uid = 0; 22 | 23 | search = new Search('uid'); 24 | search.searchIndex = new TfIdfSearchIndex('uid'); 25 | search.addIndex('title'); 26 | 27 | var titles = [ 28 | 'this document is about node.', 29 | 'this document is about ruby.', 30 | 'this document is about ruby and node.', 31 | 'this document is about node. it has node examples' 32 | ]; 33 | 34 | for (var i = 0, length = titles.length; i < length; ++i) { 35 | addDocument(titles[i]); 36 | } 37 | }); 38 | 39 | var calculateIdf = function(numDocumentsWithToken) { 40 | return 1 + Math.log(search._documents.length/(1 + numDocumentsWithToken)); 41 | }; 42 | 43 | var assertIdf = function(term, numDocumentsWithToken) { 44 | var _calculateIdf = search.searchIndex._createCalculateIdf(); 45 | expect(_calculateIdf(term, search._documents)).toEqual(calculateIdf(numDocumentsWithToken)); 46 | }; 47 | 48 | it('should handle special words like "constructor"', function () { 49 | addDocument('constructor'); 50 | }); 51 | 52 | describe('IDF', function() { 53 | it('should compute for tokens appearing only once', function() { 54 | assertIdf('and', 1); 55 | }); 56 | 57 | it('should compute for tokens appearing once in each document', function() { 58 | assertIdf('document', 4); 59 | }); 60 | 61 | it('should compute for tokens appearing multiple times in a document', function() { 62 | assertIdf('node', 3); 63 | }); 64 | 65 | it('should compute for tokens that are not within the corpus', function() { 66 | assertIdf('foobar', 0); 67 | }); 68 | 69 | it('should clear IFD cache if new documents are indexed', function() { 70 | assertIdf('ruby', 2); 71 | 72 | addDocument('this document is not about ruby.'); 73 | 74 | assertIdf('ruby', 3); 75 | }); 76 | }); 77 | 78 | var calculateTfIdf = function(numDocumentsWithToken, tokenCountInDocument) { 79 | return calculateIdf(numDocumentsWithToken) * tokenCountInDocument; 80 | }; 81 | 82 | var assertTfIdf = function(terms, document, expectedTfIdf) { 83 | var _calculateTfIdf = search.searchIndex._createCalculateTfIdf(); 84 | expect(_calculateTfIdf(terms, document, search._documents)).toEqual(expectedTfIdf); 85 | }; 86 | 87 | describe('TF-IDF', function() { 88 | it('should compute for single tokens within the corpus', function() { 89 | assertTfIdf(['node'], documents[0], calculateTfIdf(3, 1)); 90 | assertTfIdf(['node'], documents[3], calculateTfIdf(3, 2)); 91 | }); 92 | 93 | it('should compute for tokens not within the document', function() { 94 | assertTfIdf(['node'], documents[1], calculateTfIdf(3, 0)); 95 | assertTfIdf(['has node'], documents[1], calculateTfIdf(3, 0)); 96 | }); 97 | 98 | it('should compute for multiple tokens within the corpus', function() { 99 | assertTfIdf(['document', 'node'], documents[3], calculateTfIdf(4, 1) + calculateTfIdf(3, 2)); 100 | assertTfIdf(['ruby', 'and'], documents[2], calculateTfIdf(2, 1) + calculateTfIdf(1, 1)); 101 | }); 102 | 103 | it('should compute for tokens that are not within the corpus', function() { 104 | assertTfIdf(['foobar'], [], 0); 105 | assertTfIdf(['foo', 'bar'], [], 0); 106 | }); 107 | }); 108 | 109 | describe('search', function() { 110 | it('should order search results by TF-IDF descending', function() { 111 | var results = search.search('node'); 112 | 113 | expect(results.length).toEqual(3); 114 | 115 | // The 4th document has "node" twice so it should be first of the 3 116 | // The order of the other results isn't important for this test. 117 | expect(results[0]).toEqual(documents[3]); 118 | }); 119 | 120 | it('should give documents containing words with a lower IDF a higher relative ranking', function() { 121 | var documentA = addDocument('foo bar foo bar baz baz baz baz'); 122 | var documentB = addDocument('foo bar foo foo baz baz baz baz'); 123 | var documentC = addDocument('foo bar baz bar baz baz baz baz'); 124 | 125 | for (var i = 0; i < 10; i++) { 126 | addDocument('foo foo baz foo foo baz foo foo baz foo foo baz foo foo baz foo foo baz foo foo baz foo foo'); 127 | } 128 | 129 | var results = search.search('foo bar'); 130 | 131 | expect(results.length).toEqual(3); 132 | 133 | // Document A should come first because it has 2 "bar" (which have a lower total count) and 2 "foo" 134 | // Document C should come first because it has 2 "bar" (which have a lower total count) but only 1 "foo" 135 | // Document B should come last because although it has 3 "foo" it has only 1 "bar" 136 | expect(results[0]).toEqual(documentA); 137 | expect(results[1]).toEqual(documentC); 138 | expect(results[2]).toEqual(documentB); 139 | }); 140 | }); 141 | 142 | it('should support nested uid paths', function() { 143 | const melissaSmith = { 144 | name: 'Melissa Smith', 145 | login: { 146 | userId: 2562 147 | } 148 | }; 149 | const johnSmith = { 150 | name: 'John Smith', 151 | login: { 152 | userId: 54213 153 | } 154 | }; 155 | 156 | var searchIndex = new TfIdfSearchIndex(['login', 'userId']); 157 | searchIndex.indexDocument(['Melissa'], 2562, melissaSmith); 158 | searchIndex.indexDocument(['Smith'], 2562, melissaSmith); 159 | searchIndex.indexDocument(['John'], 54213, johnSmith); 160 | searchIndex.indexDocument(['Smith'], 54213, johnSmith); 161 | 162 | expect(searchIndex.search(['Melissa'], [melissaSmith, johnSmith])).toEqual([melissaSmith]); 163 | expect(searchIndex.search(['John'], [melissaSmith, johnSmith])).toEqual([johnSmith]); 164 | expect(searchIndex.search(['Smith'], [melissaSmith, johnSmith])).toEqual([melissaSmith, johnSmith]); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Installation](#installation) | 2 | [Overview](#overview) | 3 | [Tokenization](#tokenization) | 4 | [Stemming](#stemming) | 5 | [Stop Words](#stop-words) | 6 | [Search Index](#configuring-the-search-index) | 7 | [Index Strategy](#configuring-the-index-strategy) 8 | 9 | # Js Search: client-side search library 10 | 11 | Js Search enables efficient client-side searches of JavaScript and JSON objects. 12 | It is ES5 compatible and does not require jQuery or any other third-party libraries. 13 | 14 | Js Search began as a lightweight implementation of [Lunr JS](http://lunrjs.com/), offering runtime performance 15 | improvements and a smaller file size. It has since expanded to include a rich feature set- supporting stemming, 16 | stop-words, and TF-IDF ranking. 17 | 18 | Here are some JS Perf benchmarks comparing the two search libraries. (Thanks to [olivernn](https://github.com/olivernn) 19 | for tweaking the Lunr side for a better comparison!) 20 | 21 | * [Initial building of search index](http://jsperf.com/js-search-vs-lunr-js-build-search-index/5) 22 | * [Running a search](http://jsperf.com/js-search-vs-lunr-js-running-searches/5) 23 | 24 | If you're looking for a simpler, web-worker optimized JS search utility check out [js-worker-search](https://github.com/bvaughn/js-worker-search). 25 | 26 | --- 27 | ### If you like this project, 🎉 [become a sponsor](https://github.com/sponsors/bvaughn/) or ☕ [buy me a coffee](http://givebrian.coffee/) 28 | --- 29 | ### Installation 30 | 31 | You can install using either [Bower](http://bower.io/) or [NPM](https://www.npmjs.com/) like so: 32 | 33 | ```shell 34 | npm install js-search 35 | bower install js-search 36 | ``` 37 | 38 | ### Overview 39 | 40 | At a high level you configure Js Search by telling it which fields it should index for searching and then add the 41 | objects to be searched. 42 | 43 | For example, a simple use of JS Search would be as follows: 44 | 45 | ```javascript 46 | import * as JsSearch from 'js-search'; 47 | 48 | var theGreatGatsby = { 49 | isbn: '9781597226769', 50 | title: 'The Great Gatsby', 51 | author: { 52 | name: 'F. Scott Fitzgerald' 53 | }, 54 | tags: ['book', 'inspirational'] 55 | }; 56 | var theDaVinciCode = { 57 | isbn: '0307474275', 58 | title: 'The DaVinci Code', 59 | author: { 60 | name: 'Dan Brown' 61 | }, 62 | tags: ['book', 'mystery'] 63 | }; 64 | var angelsAndDemons = { 65 | isbn: '074349346X', 66 | title: 'Angels & Demons', 67 | author: { 68 | name: 'Dan Brown', 69 | }, 70 | tags: ['book', 'mystery'] 71 | }; 72 | 73 | var search = new JsSearch.Search('isbn'); 74 | search.addIndex('title'); 75 | search.addIndex(['author', 'name']); 76 | search.addIndex('tags') 77 | 78 | search.addDocuments([theGreatGatsby, theDaVinciCode, angelsAndDemons]); 79 | 80 | search.search('The'); // [theGreatGatsby, theDaVinciCode] 81 | search.search('scott'); // [theGreatGatsby] 82 | search.search('dan'); // [angelsAndDemons, theDaVinciCode] 83 | search.search('mystery') // [angelsAndDemons, theDaVinciCode] 84 | ``` 85 | 86 | ### Tokenization 87 | 88 | Tokenization is the process of breaking text (e.g. sentences) into smaller, searchable tokens (e.g. words or parts of 89 | words). Js Search provides a basic tokenizer that should work well for English but you can provide your own like so: 90 | 91 | ```javascript 92 | search.tokenizer = { 93 | tokenize( text /* string */ ) { 94 | // Convert text to an Array of strings and return the Array 95 | } 96 | }; 97 | ``` 98 | 99 | ### Stemming 100 | 101 | Stemming is the process of reducing search tokens to their root (or "stem") so that searches for different forms of a 102 | word will still yield results. For example "search", "searching" and "searched" can all be reduced to the stem "search". 103 | 104 | Js Search does not implement its own stemming library but it does support stemming through the use of third-party 105 | libraries. 106 | 107 | To enable stemming, use the `StemmingTokenizer` like so: 108 | 109 | ```javascript 110 | var stemmer = require('porter-stemmer').stemmer; 111 | 112 | search.tokenizer = 113 | new JsSearch.StemmingTokenizer( 114 | stemmer, // Function should accept a string param and return a string 115 | new JsSearch.SimpleTokenizer()); 116 | ``` 117 | 118 | ### Stop Words 119 | 120 | Stop words are very common (e.g. a, an, and, the, of) and are often not semantically meaningful. By default Js Search 121 | does not filter these words, but filtering can be enabled by using the `StopWordsTokenizer` like so: 122 | 123 | ```javascript 124 | search.tokenizer = 125 | new JsSearch.StopWordsTokenizer( 126 | new JsSearch.SimpleTokenizer()); 127 | ``` 128 | 129 | By default Js Search uses a slightly modified version of the Google History stop words listed on 130 | [www.ranks.nl/stopwords](http://www.ranks.nl/stopwords). You can modify this list of stop words by adding or removing 131 | values from the `JsSearch.StopWordsMap` object like so: 132 | 133 | ```javascript 134 | JsSearch.StopWordsMap.the = false; // Do not treat "the" as a stop word 135 | JsSearch.StopWordsMap.bob = true; // Treat "bob" as a stop word 136 | ``` 137 | 138 | Note that stop words are lower case and so using a case-sensitive sanitizer may prevent some stop words from being 139 | removed. 140 | 141 | ### Configuring the search index 142 | 143 | There are two search indices packaged with `js-search`. 144 | 145 | Term frequency–inverse document frequency (or TF-IDF) is a numeric statistic intended to reflect how important a word 146 | (or words) are to a document within a corpus. The TF-IDF value increases proportionally to the number of times a word 147 | appears in the document but is offset by the frequency of the word in the corpus. This helps to adjust for the fact that 148 | some words (e.g. and, or, the) appear more frequently than others. 149 | 150 | By default Js Search supports TF-IDF ranking but this can be disabled for performance reasons if it is not required. You 151 | can specify an alternate [`ISearchIndex`](https://github.com/bvaughn/js-search/blob/master/source/SearchIndex/SearchIndex.js) 152 | implementation in order to disable TF-IDF, like so: 153 | 154 | ```javascript 155 | // default 156 | search.searchIndex = new JsSearch.TfIdfSearchIndex(); 157 | 158 | // Search index capable of returning results matching a set of tokens 159 | // but without any meaningful rank or order. 160 | search.searchIndex = new JsSearch.UnorderedSearchIndex(); 161 | ``` 162 | 163 | ### Configuring the index strategy 164 | 165 | There are three index strategies packaged with `js-search`. 166 | 167 | `PrefixIndexStrategy` indexes for prefix searches. 168 | (e.g. the term "cat" is indexed as "c", "ca", and "cat" allowing prefix search lookups). 169 | 170 | `AllSubstringsIndexStrategy` indexes for all substrings. In other word "c", "ca", "cat", "a", "at", and "t" all match "cat". 171 | 172 | `ExactWordIndexStrategy` indexes for exact word matches. For example "bob" will match "bob jones" (but "bo" will not). 173 | 174 | By default Js Search supports prefix indexing but this is configurable. You 175 | can specify an alternate [`IIndexStrategy`](https://github.com/bvaughn/js-search/blob/master/source/IndexStrategy/IndexStrategy.js) 176 | implementation in order to disable prefix indexing, like so: 177 | 178 | ```javascript 179 | // default 180 | search.indexStrategy = new JsSearch.PrefixIndexStrategy(); 181 | 182 | // this index strategy is built for all substrings matches. 183 | search.indexStrategy = new JsSearch.AllSubstringsIndexStrategy(); 184 | 185 | // this index strategy is built for exact word matches. 186 | search.indexStrategy = new JsSearch.ExactWordIndexStrategy(); 187 | ``` 188 | -------------------------------------------------------------------------------- /source/Search.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import getNestedFieldValue from './getNestedFieldValue'; 3 | 4 | import { PrefixIndexStrategy } from './IndexStrategy/index'; 5 | import { LowerCaseSanitizer } from './Sanitizer/index'; 6 | import { TfIdfSearchIndex } from './SearchIndex/index'; 7 | import { SimpleTokenizer } from './Tokenizer/index'; 8 | 9 | import type { IIndexStrategy } from './IndexStrategy/index'; 10 | import type { ISanitizer } from './Sanitizer/index'; 11 | import type { ISearchIndex } from './SearchIndex/index'; 12 | import type { ITokenizer } from './Tokenizer/index'; 13 | 14 | /** 15 | * Simple client-side searching within a set of documents. 16 | * 17 | *

Documents can be searched by any number of fields. Indexing and search strategies are highly customizable. 18 | */ 19 | export class Search { 20 | 21 | _documents : Array; 22 | _indexStrategy : IIndexStrategy; 23 | _initialized : boolean; 24 | _sanitizer : ISanitizer; 25 | 26 | /** 27 | * Array containing either a property name or a path (list of property names) to a nested value 28 | */ 29 | _searchableFields : Array>; 30 | 31 | _searchIndex : ISearchIndex; 32 | _tokenizer : ITokenizer; 33 | _uidFieldName : string | Array; 34 | 35 | /** 36 | * Constructor. 37 | * @param uidFieldName Field containing values that uniquely identify search documents; this field's values are used 38 | * to ensure that a search result set does not contain duplicate objects. 39 | */ 40 | constructor(uidFieldName : string | Array) { 41 | if (!uidFieldName) { 42 | throw Error('js-search requires a uid field name constructor parameter'); 43 | } 44 | 45 | this._uidFieldName = uidFieldName; 46 | 47 | // Set default/recommended strategies 48 | this._indexStrategy = new PrefixIndexStrategy(); 49 | this._searchIndex = new TfIdfSearchIndex(uidFieldName); 50 | this._sanitizer = new LowerCaseSanitizer(); 51 | this._tokenizer = new SimpleTokenizer(); 52 | 53 | this._documents = []; 54 | this._searchableFields = []; 55 | } 56 | 57 | /** 58 | * Override the default index strategy. 59 | * @param value Custom index strategy 60 | * @throws Error if documents have already been indexed by this search instance 61 | */ 62 | set indexStrategy(value : IIndexStrategy) { 63 | if (this._initialized) { 64 | throw Error('IIndexStrategy cannot be set after initialization'); 65 | } 66 | 67 | this._indexStrategy = value; 68 | } 69 | 70 | get indexStrategy() : IIndexStrategy { 71 | return this._indexStrategy; 72 | } 73 | 74 | /** 75 | * Override the default text sanitizing strategy. 76 | * @param value Custom text sanitizing strategy 77 | * @throws Error if documents have already been indexed by this search instance 78 | */ 79 | set sanitizer(value : ISanitizer) { 80 | if (this._initialized) { 81 | throw Error('ISanitizer cannot be set after initialization'); 82 | } 83 | 84 | this._sanitizer = value; 85 | } 86 | get sanitizer() : ISanitizer { 87 | return this._sanitizer; 88 | } 89 | 90 | /** 91 | * Override the default search index strategy. 92 | * @param value Custom search index strategy 93 | * @throws Error if documents have already been indexed 94 | */ 95 | set searchIndex(value : ISearchIndex) { 96 | if (this._initialized) { 97 | throw Error('ISearchIndex cannot be set after initialization'); 98 | } 99 | 100 | this._searchIndex = value; 101 | } 102 | get searchIndex() : ISearchIndex { 103 | return this._searchIndex; 104 | } 105 | 106 | /** 107 | * Override the default text tokenizing strategy. 108 | * @param value Custom text tokenizing strategy 109 | * @throws Error if documents have already been indexed by this search instance 110 | */ 111 | set tokenizer(value : ITokenizer) { 112 | if (this._initialized) { 113 | throw Error('ITokenizer cannot be set after initialization'); 114 | } 115 | 116 | this._tokenizer = value; 117 | } 118 | get tokenizer() : ITokenizer { 119 | return this._tokenizer; 120 | } 121 | 122 | /** 123 | * Add a searchable document to the index. Document will automatically be indexed for search. 124 | * @param document 125 | */ 126 | addDocument(document : Object) : void { 127 | this.addDocuments([document]); 128 | } 129 | 130 | /** 131 | * Adds searchable documents to the index. Documents will automatically be indexed for search. 132 | * @param document 133 | */ 134 | addDocuments(documents : Array) : void { 135 | this._documents = this._documents.concat(documents); 136 | this.indexDocuments_(documents, this._searchableFields); 137 | } 138 | 139 | /** 140 | * Add a new searchable field to the index. Existing documents will automatically be indexed using this new field. 141 | * 142 | * @param field Searchable field or field path. Pass a string to index a top-level field and an array of strings for nested fields. 143 | */ 144 | addIndex(field : string|Array) { 145 | this._searchableFields.push(field); 146 | this.indexDocuments_(this._documents, [field]); 147 | } 148 | 149 | /** 150 | * Search all documents for ones matching the specified query text. 151 | * @param query 152 | * @returns {Array} 153 | */ 154 | search(query : string) : Array { 155 | var tokens : Array = this._tokenizer.tokenize(this._sanitizer.sanitize(query)); 156 | 157 | return this._searchIndex.search(tokens, this._documents); 158 | } 159 | 160 | /** 161 | * @param documents 162 | * @param _searchableFields Array containing property names and paths (lists of property names) to nested values 163 | * @private 164 | */ 165 | indexDocuments_(documents : Array, _searchableFields : Array>) : void { 166 | this._initialized = true; 167 | 168 | var indexStrategy = this._indexStrategy; 169 | var sanitizer = this._sanitizer; 170 | var searchIndex = this._searchIndex; 171 | var tokenizer = this._tokenizer; 172 | var uidFieldName = this._uidFieldName; 173 | 174 | for (var di = 0, numDocuments = documents.length; di < numDocuments; di++) { 175 | var doc = documents[di]; 176 | var uid; 177 | 178 | if (uidFieldName instanceof Array) { 179 | uid = getNestedFieldValue(doc, uidFieldName); 180 | } else { 181 | uid = doc[uidFieldName]; 182 | } 183 | 184 | for (var sfi = 0, numSearchableFields = _searchableFields.length; sfi < numSearchableFields; sfi++) { 185 | var fieldValue; 186 | var searchableField = _searchableFields[sfi]; 187 | 188 | if (searchableField instanceof Array) { 189 | fieldValue = getNestedFieldValue(doc, searchableField); 190 | } else { 191 | fieldValue = doc[searchableField]; 192 | } 193 | 194 | if ( 195 | fieldValue != null && 196 | typeof fieldValue !== 'string' && 197 | fieldValue.toString 198 | ) { 199 | fieldValue = fieldValue.toString(); 200 | } 201 | 202 | if (typeof fieldValue === 'string') { 203 | var fieldTokens = tokenizer.tokenize(sanitizer.sanitize(fieldValue)); 204 | 205 | for (var fti = 0, numFieldValues = fieldTokens.length; fti < numFieldValues; fti++) { 206 | var fieldToken = fieldTokens[fti]; 207 | var expandedTokens = indexStrategy.expandToken(fieldToken); 208 | 209 | for (var eti = 0, nummExpandedTokens = expandedTokens.length; eti < nummExpandedTokens; eti++) { 210 | var expandedToken = expandedTokens[eti]; 211 | 212 | searchIndex.indexDocument(expandedToken, uid, doc); 213 | } 214 | } 215 | } 216 | } 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /benchmarks/books.json: -------------------------------------------------------------------------------- 1 | { 2 | "books": [ 3 | { 4 | "isbn": "0520203259", 5 | "title": "Hellenistic history and culture", 6 | "author": "Peter Green" 7 | }, 8 | { 9 | "isbn": "0300088108", 10 | "title": "Plutarch", 11 | "author": "Robert Lamberton" 12 | }, 13 | { 14 | "isbn": "0520204832", 15 | "title": "Studies in Greek culture and Roman policy", 16 | "author": "Erich S. Gruen" 17 | }, 18 | { 19 | "isbn": "0819163015", 20 | "title": "Past and future in ancient history", 21 | "author": "Chester G. Starr" 22 | }, 23 | { 24 | "isbn": "0140445625", 25 | "title": "A short account of the destruction of the Indies", 26 | "author": "Bartolomé de las Casas" 27 | }, 28 | { 29 | "isbn": "0521228298", 30 | "title": "The Hellenistic world from Alexander to the Roman conquest : a selection of ancient sources in transla", 31 | "author": "M. M Austin" 32 | }, 33 | { 34 | "isbn": "0500400288", 35 | "title": "Philip II and Macedonian imperialism", 36 | "author": "John R. Ellis" 37 | }, 38 | { 39 | "isbn": "0713990597", 40 | "title": "The historical figure of Jesus", 41 | "author": "E. P. Sanders" 42 | }, 43 | { 44 | "isbn": "1555406211", 45 | "title": "Latin in American schools : teaching the ancient world", 46 | "author": "Sally Davis" 47 | }, 48 | { 49 | "isbn": "0691055491", 50 | "title": "In the shadow of Olympus : the emergence of Macedon", 51 | "author": "Eugene N. Borza" 52 | }, 53 | { 54 | "isbn": "0486206459", 55 | "title": "Virtruvius, the ten books on architecture,", 56 | "author": "Vitruvius Pollio" 57 | }, 58 | { 59 | "isbn": "0801846196", 60 | "title": "The Search for the ancient novel", 61 | "author": "James Tatum" 62 | }, 63 | { 64 | "isbn": "0800631641", 65 | "title": "Herod : king of the Jews and friend of the Romans", 66 | "author": "Peter Richardson" 67 | }, 68 | { 69 | "isbn": "0773508376", 70 | "title": "Theopompus the historian", 71 | "author": "Gordon Spencer Shrimpton" 72 | }, 73 | { 74 | "isbn": "0472064185", 75 | "title": "Hellenism in Late Antiquity", 76 | "author": "G. W. Bowersock" 77 | }, 78 | { 79 | "isbn": "0198148283", 80 | "title": "A historical commentary on Arrian's History of Alexander", 81 | "author": "A. B. Bosworth" 82 | }, 83 | { 84 | "isbn": "0195076184", 85 | "title": "The Oxford companion to archaeology", 86 | "author": "Brian M. Fagan" 87 | }, 88 | { 89 | "isbn": "0691067570", 90 | "title": "Xenophon's imperial fiction : on the education of Cyrus", 91 | "author": "James Tatum" 92 | }, 93 | { 94 | "isbn": "0691040605", 95 | "title": "The Periplus Maris Erythraei : text with introduction, translation, and commentary", 96 | "author": "Lionel Casson" 97 | }, 98 | { 99 | "isbn": "0520085205", 100 | "title": "Moral vision in the Histories of Polybius", 101 | "author": "Arthur M. Eckstein" 102 | }, 103 | { 104 | "isbn": "080706792X", 105 | "title": "The myth of matriarchal prehistory : why an invented past won't give women a future", 106 | "author": "Cynthia Eller" 107 | }, 108 | { 109 | "isbn": "037550432X", 110 | "title": "Route 66 A.D. : on the trail of ancient Roman tourists", 111 | "author": "Tony Perrottet" 112 | }, 113 | { 114 | "isbn": "0520063198", 115 | "title": "A history of Macedonia", 116 | "author": "R. M. Errington" 117 | }, 118 | { 119 | "isbn": "0631174729", 120 | "title": "A history of ancient Egypt", 121 | "author": "Nicolas-Christophe Grimal" 122 | }, 123 | { 124 | "isbn": "0674474910", 125 | "title": "The Jews in the Greek Age", 126 | "author": "E. J. Bickerman" 127 | }, 128 | { 129 | "isbn": "0520069811", 130 | "title": "Polybius, by F. W. Walbank", 131 | "author": "F. W. Walbank" 132 | }, 133 | { 134 | "isbn": "0292750315", 135 | "title": "Magic, witchcraft, and curing", 136 | "author": "John Middleton, comp" 137 | }, 138 | { 139 | "isbn": "0815550294", 140 | "title": "The interpretation of dreams = Oneirocritica", 141 | "author": "Daldianus Artemidorus" 142 | }, 143 | { 144 | "isbn": "0517277956", 145 | "title": "The lost books of the Bible : being all the Gospels, Epistles, and other pieces now extant attributed", 146 | "author": "William Hone" 147 | }, 148 | { 149 | "isbn": "0684106027", 150 | "title": "Religion and the decline of magic [by] Keith Thomas", 151 | "author": "Keith Thomas" 152 | }, 153 | { 154 | "isbn": "0395549671", 155 | "title": "Oracle bones, stars, and wheelbarrows : ancient Chinese science and technology", 156 | "author": "Frank Xavier Ross" 157 | }, 158 | { 159 | "isbn": "0691000921", 160 | "title": "The hieroglyphics of Horapollo", 161 | "author": "Horapollo" 162 | }, 163 | { 164 | "isbn": "0851154468", 165 | "title": "A history of western astrology", 166 | "author": "S. J. Tester" 167 | }, 168 | { 169 | "isbn": "0393005445", 170 | "title": "The Hellenistic age; aspects of Hellenistic civilization, treated by J. B. Bury ... E. A. Barber ...", 171 | "author": "J. B. Bury" 172 | }, 173 | { 174 | "isbn": "0691057508", 175 | "title": "Religions of late antiquity in practice", 176 | "author": "Richard Valantasis" 177 | }, 178 | { 179 | "isbn": "0415202078", 180 | "title": "Magic in the Roman world : pagans, Jews, and Christians", 181 | "author": "Naomi Janowitz" 182 | }, 183 | { 184 | "isbn": "0520020219", 185 | "title": "The occult sciences in the Renaissance; a study in intellectual patterns", 186 | "author": "Wayne Shumaker" 187 | }, 188 | { 189 | "isbn": "0226075915", 190 | "title": "Love between women : early Christian responses to female homoeroticism", 191 | "author": "Bernadette J. Brooten" 192 | }, 193 | { 194 | "isbn": "0891303464", 195 | "title": "Sources for the study of Greek religion", 196 | "author": "David Gerard Rice" 197 | }, 198 | { 199 | "isbn": "0807611379", 200 | "title": "Witchcraft at Salem", 201 | "author": "Chadwick Hansen" 202 | }, 203 | { 204 | "isbn": "0394554957", 205 | "title": "Pagans and Christians", 206 | "author": "Robin Lane Fox" 207 | }, 208 | { 209 | "isbn": "0674936868", 210 | "title": "The Victorians and ancient Greece", 211 | "author": "Richard Jenkyns" 212 | }, 213 | { 214 | "isbn": "0226067270", 215 | "title": "Mesopotamia : writing, reasoning, and the gods", 216 | "author": "Jean Bottéro" 217 | }, 218 | { 219 | "isbn": "067464364X", 220 | "title": "The orientalizing revolution : Near Eastern influence on Greek culture in the early archaic age", 221 | "author": "Walter Burkert" 222 | }, 223 | { 224 | "isbn": "0472087622", 225 | "title": "The rise and fall of states according to Greek authors", 226 | "author": "Jacqueline de Romilly" 227 | }, 228 | { 229 | "isbn": "0521785766", 230 | "title": "Magic in the Middle Ages", 231 | "author": "Richard Kieckhefer" 232 | }, 233 | { 234 | "isbn": "0890895759", 235 | "title": "The world of classical myth : gods and goddesses, heroines and heroes", 236 | "author": "Carl A. P. Ruck" 237 | }, 238 | { 239 | "isbn": "1395564727", 240 | "title": "The origin of consciousness in the breakdown of the bicameral mind", 241 | "author": "Julian Jaynes" 242 | }, 243 | { 244 | "isbn": "0415249821", 245 | "title": "Magic and magicians in the Greco-Roman world", 246 | "author": "Matthew Dickie" 247 | }, 248 | { 249 | "isbn": "0195141830", 250 | "title": "Lost Christianities : the battle for Scripture and the faiths we never knew", 251 | "author": "Bart D. Ehrman" 252 | }, 253 | { 254 | "isbn": "0819551732", 255 | "title": "On pagans, Jews, and Christians", 256 | "author": "Arnaldo Momigliano" 257 | }, 258 | { 259 | "isbn": "0195141822", 260 | "title": "Lost scriptures : books that did not make it into the New Testament", 261 | "author": "Bart D Ehrman" 262 | }, 263 | { 264 | "isbn": "0674750756", 265 | "title": "Rebecca's children : Judaism and Christianity in the Roman world", 266 | "author": "Alan F. Segal" 267 | }, 268 | { 269 | "isbn": "0195115678", 270 | "title": "A preface to Mark : notes on the Gospel in its literary and cultural settings", 271 | "author": "Christopher Bryan" 272 | }, 273 | { 274 | "isbn": "019825153X", 275 | "title": "Roman society and Roman law in the New Testament", 276 | "author": "A. N. Sherwin-White" 277 | }, 278 | { 279 | "isbn": "0517055902", 280 | "title": "How to read the Bible : two volumes in one", 281 | "author": "Etienne Charpentier" 282 | }, 283 | { 284 | "isbn": "0195141571", 285 | "title": "Saint Saul : a skeleton key to the historical Jesus", 286 | "author": "Donald H. Akenson" 287 | }, 288 | { 289 | "isbn": "0195124731", 290 | "title": "Jesus, apocalyptic prophet of the new millennium", 291 | "author": "Bart D. Ehrman" 292 | }, 293 | { 294 | "isbn": "3110146932", 295 | "title": "Introduction to the New Testament", 296 | "author": "Helmut Koester" 297 | }, 298 | { 299 | "isbn": "0385264259", 300 | "title": "A marginal Jew : rethinking the historical Jesus", 301 | "author": "John P. Meier" 302 | }, 303 | { 304 | "isbn": "0830815449", 305 | "title": "The Jesus quest : the third search for the Jew of Nazareth", 306 | "author": "Ben Witherington, III" 307 | }, 308 | { 309 | "isbn": "0195126394", 310 | "title": "The New Testament : a historical introduction to the early Christian writings", 311 | "author": "Bart D. Ehrman" 312 | }, 313 | { 314 | "isbn": "0809125366", 315 | "title": "In the days of Jesus : the Jewish background and unique teaching of Jesus", 316 | "author": "Anthony J. Tambasco" 317 | }, 318 | { 319 | "isbn": "0140135022", 320 | "title": "The Romans", 321 | "author": "R. H. Barrow, b. 1893" 322 | }, 323 | { 324 | "isbn": "0198810016", 325 | "title": "The Roman revolution", 326 | "author": "Ronald Syme, Sir" 327 | }, 328 | { 329 | "isbn": "0895220261", 330 | "title": "The Ides of March", 331 | "author": "Naphtali Lewis" 332 | }, 333 | { 334 | "isbn": "0404574661", 335 | "title": "Sexual life in ancient Rome", 336 | "author": "Otto Kiefer" 337 | }, 338 | { 339 | "isbn": "9754790892", 340 | "title": "Lycia: Western Section of the Southern Coast of Turkey", 341 | "author": "Ülgür Önen" 342 | }, 343 | { 344 | "isbn": "0674663241", 345 | "title": "Persuasions of the witch's craft : ritual magic in contemporary England", 346 | "author": "T. M. Luhrmann" 347 | }, 348 | { 349 | "isbn": "0715617478", 350 | "title": "Suetonius, the scholar and his Caesars", 351 | "author": "Andrew Wallace-Hadrill" 352 | }, 353 | { 354 | "isbn": "067451193X", 355 | "title": "The later Roman empire, AD 284-430", 356 | "author": "Averil Cameron" 357 | }, 358 | { 359 | "isbn": "0801837014", 360 | "title": "The social history of Rome", 361 | "author": "Géza Alföldy" 362 | }, 363 | { 364 | "isbn": "0674777700", 365 | "title": "The Roman Empire", 366 | "author": "C. M. Wells" 367 | }, 368 | { 369 | "isbn": "0415096782", 370 | "title": "Spectacles of death in ancient Rome", 371 | "author": "Donald G. Kyle" 372 | }, 373 | { 374 | "isbn": "0520201531", 375 | "title": "The last generation of the Roman Republic [by] Erich S. Gruen", 376 | "author": "Erich S. Gruen" 377 | }, 378 | { 379 | "isbn": "080184200X", 380 | "title": "The Roman family", 381 | "author": "Suzanne Dixon" 382 | }, 383 | { 384 | "isbn": "0674779274", 385 | "title": "The Roman republic", 386 | "author": "Michael H. Crawford" 387 | }, 388 | { 389 | "isbn": "0312199589", 390 | "title": "Romans and Barbarians : four views from the empire's edge, 1st century A.D.", 391 | "author": "Derek Williams" 392 | }, 393 | { 394 | "isbn": "039587596X", 395 | "title": "The road to Ubar : finding the Atlantis of the sands", 396 | "author": "Nicholas Clapp" 397 | }, 398 | { 399 | "isbn": "1556112556", 400 | "title": "The Marvell College murders : a novel", 401 | "author": "Sophie Belfort" 402 | }, 403 | { 404 | "isbn": "1555831680", 405 | "title": "In the land of Alexander : gay travels, with history and politics, in Hungary, Yugoslavia, Turkey, and", 406 | "author": "Keith Hale" 407 | }, 408 | { 409 | "isbn": "0674010191", 410 | "title": "The Pantheon : design, meaning, and progeny", 411 | "author": "William Lloyd MacDonald" 412 | }, 413 | { 414 | "isbn": "0385486472", 415 | "title": "When in Rome : a journal of life in Vatican City", 416 | "author": "Robert J. Hutchinson" 417 | }, 418 | { 419 | "isbn": "0060799048", 420 | "title": "Walking the Bible : a photographic journey", 421 | "author": "Bruce S. Feiler" 422 | }, 423 | { 424 | "isbn": "0714119105", 425 | "title": "How to read Egyptian hieroglyphs : a step-by-step guide to teach yourself", 426 | "author": "Mark Collier" 427 | }, 428 | { 429 | "isbn": "0684810522", 430 | "title": "Noah's flood : the new scientific discoveries about the event that changed history", 431 | "author": "William B. F. Ryan" 432 | }, 433 | { 434 | "isbn": "0140244611", 435 | "title": "Istanbul", 436 | "author": "John Freely" 437 | }, 438 | { 439 | "isbn": "1858288770", 440 | "title": "The rough guide to Portugal", 441 | "author": "Mark Ellingham" 442 | }, 443 | { 444 | "isbn": "0671779974", 445 | "title": "Turkish reflections : a biography of a place", 446 | "author": "Mary Lee Settle" 447 | }, 448 | { 449 | "isbn": "0670800074", 450 | "title": "Journey to Kars", 451 | "author": "Philip Glazebrook" 452 | }, 453 | { 454 | "isbn": "0198140959", 455 | "title": "The kingdom of the Hittites", 456 | "author": "Trevor Bryce" 457 | }, 458 | { 459 | "isbn": "1565412176", 460 | "title": "Life in Alanya : Turkish delight", 461 | "author": "George Crews McGhee" 462 | }, 463 | { 464 | "isbn": "0486212181", 465 | "title": "Personal narrative of a pilgrimage to al-Madinah & Meccah, by Sir Richard F. Burton. Edited by his wif", 466 | "author": "Richard Francis Burton, Sir" 467 | }, 468 | { 469 | "isbn": "0521633842", 470 | "title": "The rise and rule of Tamerlane", 471 | "author": "Beatrice Forbes Manz" 472 | }, 473 | { 474 | "isbn": "0192802992", 475 | "title": "Sacred signs : hieroglyphs in ancient Egypt", 476 | "author": "Penelope Wilson" 477 | }, 478 | { 479 | "isbn": "0140062718", 480 | "title": "The rebel angels", 481 | "author": "Robertson Davies" 482 | }, 483 | { 484 | "isbn": "0156001314", 485 | "title": "The name of the rose", 486 | "author": "Umberto Eco" 487 | }, 488 | { 489 | "isbn": "1400031427", 490 | "title": "In search of Zarathustra : the first prophet and the ideas that changed the world", 491 | "author": "Paul Kriwaczek" 492 | }, 493 | { 494 | "isbn": "0716730901", 495 | "title": "Why people believe weird things : pseudoscience, superstition, and other confusions of our time", 496 | "author": "Michael Shermer" 497 | }, 498 | { 499 | "isbn": "0802116116", 500 | "title": "One thousand roads to Mecca : ten centuries of travelers writing about the Muslim pilgrimage", 501 | "author": "Michael Wolfe" 502 | }, 503 | { 504 | "isbn": "0195116674", 505 | "title": "Oliver Wendell Holmes : sage of the Supreme Court", 506 | "author": "G. Edward White" 507 | }, 508 | { 509 | "isbn": "0802713424", 510 | "title": "null", 511 | "author": "Tom Standage" 512 | }, 513 | { 514 | "isbn": "071954663X", 515 | "title": "Turkey beyond the Maeander", 516 | "author": "George Ewart Bean" 517 | }, 518 | { 519 | "isbn": "0395328047", 520 | "title": "A history of Western society", 521 | "author": "John P. McKay" 522 | }, 523 | { 524 | "isbn": "0810928191", 525 | "title": "Knossos : searching for the legendary palace of King Minos", 526 | "author": "Alexandre Farnoux" 527 | }, 528 | { 529 | "isbn": "0810928396", 530 | "title": "In search of ancient Rome", 531 | "author": "Claude Moatti" 532 | }, 533 | { 534 | "isbn": "0460107887", 535 | "title": "Leadership and the cult of the personality", 536 | "author": "Jane F Gardner" 537 | }, 538 | { 539 | "isbn": "0520231236", 540 | "title": "Spartan reflections", 541 | "author": "Paul Cartledge" 542 | }, 543 | { 544 | "isbn": "0684828154", 545 | "title": "The landmark Thucydides : a comprehensive guide to the Peloponnesian War", 546 | "author": "Thucydides" 547 | }, 548 | { 549 | "isbn": "0801413672", 550 | "title": "The Peace of Nicias and the Sicilian Expedition", 551 | "author": "Donald Kagan" 552 | }, 553 | { 554 | "isbn": "0684863952", 555 | "title": "Pericles of Athens and the birth of democracy", 556 | "author": "Donald Kagan" 557 | }, 558 | { 559 | "isbn": "0674587383", 560 | "title": "Moses the Egyptian : the memory of Egypt in western monotheism", 561 | "author": "Jan Assmann" 562 | }, 563 | { 564 | "isbn": "0810928868", 565 | "title": "Mummies : a voyage through eternity", 566 | "author": "Françoise Dunand" 567 | }, 568 | { 569 | "isbn": "0816027617", 570 | "title": "Pirates! : brigands, buccaneers, and privateers in fact, fiction, and legend", 571 | "author": "Jan Rogoziânski" 572 | }, 573 | { 574 | "isbn": "157392251X", 575 | "title": "The love songs of Sappho", 576 | "author": "Sappho" 577 | }, 578 | { 579 | "isbn": "0404146678", 580 | "title": "The mercenaries of the Hellenistic world", 581 | "author": "G. T. Griffith" 582 | }, 583 | { 584 | "isbn": "1400040930", 585 | "title": "Of paradise and power : America and Europe in the new world order", 586 | "author": "Robert Kagan" 587 | }, 588 | { 589 | "isbn": "0312261764", 590 | "title": "The illustrated story of copyright", 591 | "author": "Edward B. Samuels" 592 | }, 593 | { 594 | "isbn": "0764504606", 595 | "title": "Perl for dummies", 596 | "author": "Paul Hoffman" 597 | }, 598 | { 599 | "isbn": "0735712565", 600 | "title": "Search engine visibility", 601 | "author": "Shari Thurow" 602 | }, 603 | { 604 | "isbn": "0451527933", 605 | "title": "The metamorphoses", 606 | "author": "Ovid" 607 | }, 608 | { 609 | "isbn": "0888444702", 610 | "title": "The Vulgate commentary on Ovid's Metamorphoses : the creation myth and the story of Orpheus", 611 | "author": "Frank Thomas Coulson" 612 | }, 613 | { 614 | "isbn": "0870230395", 615 | "title": "The Symposium of Plato. Translated by Suzy Q. Groden. Edited by John A. Brentlinger. Drawings by Leona", 616 | "author": "Plato" 617 | }, 618 | { 619 | "isbn": "0767917367", 620 | "title": "The telephone booth Indian", 621 | "author": "A. J. Liebling" 622 | }, 623 | { 624 | "isbn": "0385419945", 625 | "title": "Silicon snake oil : second thoughts on the information highway", 626 | "author": "Clifford Stoll" 627 | }, 628 | { 629 | "isbn": "0618319387", 630 | "title": "The nature of science : an A-Z guide to the laws and principles governing our universe", 631 | "author": "James S. Trefil" 632 | }, 633 | { 634 | "isbn": "067400602X", 635 | "title": "Oversold and underused : computers in the classroom", 636 | "author": "Larry Cuban" 637 | }, 638 | { 639 | "isbn": "1587991039", 640 | "title": "Technomanifestos : visions from the information revolutionaries", 641 | "author": "Adam Brate" 642 | }, 643 | { 644 | "isbn": "0281055513", 645 | "title": "The resurrection of the Son of God", 646 | "author": "N. T. Wright" 647 | }, 648 | { 649 | "isbn": "0738205435", 650 | "title": "Small pieces, loosely joined : a unified theory of the web", 651 | "author": "David Weinberger" 652 | }, 653 | { 654 | "isbn": "0851709680", 655 | "title": "The Shawshank redemption", 656 | "author": "Mark Kermode" 657 | }, 658 | { 659 | "isbn": "0395891698", 660 | "title": "The war against parents : what we can do for America's beleaguered moms and dads", 661 | "author": "Sylvia Ann Hewlett" 662 | }, 663 | { 664 | "isbn": "0374525749", 665 | "title": "The Odyssey", 666 | "author": "Homer" 667 | }, 668 | { 669 | "isbn": "1568984308", 670 | "title": "You are here : personal geographies and other maps of the imagination", 671 | "author": "Katharine A. Harmon" 672 | }, 673 | { 674 | "isbn": "0671793853", 675 | "title": "Time detectives : how archeologists use technology to recapture the past", 676 | "author": "Brian M. Fagan" 677 | }, 678 | { 679 | "isbn": "0802713912", 680 | "title": "The Turk : the life and times of the famous eighteenth-century chess-playing machine", 681 | "author": "Tom Standage" 682 | }, 683 | { 684 | "isbn": "0345372050", 685 | "title": "You just don't understand : women and men in conversation", 686 | "author": "Deborah Tannen" 687 | }, 688 | { 689 | "isbn": "0375501517", 690 | "title": "The island of lost maps : a true story of cartographic crime", 691 | "author": "Miles Harvey" 692 | }, 693 | { 694 | "isbn": "019512023X", 695 | "title": "Plutarch's Advice to the bride and groom, and A consolation to his wife : English translations, commen", 696 | "author": "Sarah B. Pomeroy" 697 | }, 698 | { 699 | "isbn": "0060984031", 700 | "title": "The third chimpanzee : the evolution and future of the human animal", 701 | "author": "Jared M. Diamond" 702 | }, 703 | { 704 | "isbn": "0155948911", 705 | "title": "Vice & virtue in everyday life : introductory readings in ethics", 706 | "author": "Christina Hoff Sommers" 707 | }, 708 | { 709 | "isbn": "0618313370", 710 | "title": "Toward a new Catholic Church : the promise of reform", 711 | "author": "James Carroll" 712 | }, 713 | { 714 | "isbn": "0192852108", 715 | "title": "A history of heresy", 716 | "author": "David Christie-Murray" 717 | }, 718 | { 719 | "isbn": "0877936536", 720 | "title": "This is our faith : a Catholic catechism for adults", 721 | "author": "Michael Pennock" 722 | }, 723 | { 724 | "isbn": "0618226478", 725 | "title": "The new dictionary of cultural literacy", 726 | "author": "E. D. Hirsch" 727 | }, 728 | { 729 | "isbn": "1565927699", 730 | "title": "PHP pocket reference", 731 | "author": "Rasmus Lerdorf" 732 | }, 733 | { 734 | "isbn": "0195283481", 735 | "title": "The new Oxford annotated Bible with the Apocrypha : Revised standard version, containing the second ed", 736 | "author": "Herbert Gordon May" 737 | }, 738 | { 739 | "isbn": "081090117X", 740 | "title": "The new illustrated encyclopedia of world history", 741 | "author": "William L. Langer" 742 | }, 743 | { 744 | "isbn": "0078825598", 745 | "title": "HTML programmer's reference", 746 | "author": "Thomas A. Powell" 747 | }, 748 | { 749 | "isbn": "0596000480", 750 | "title": "JavaScript : the definitive guide", 751 | "author": "David Flanagan" 752 | }, 753 | { 754 | "isbn": "1565926102", 755 | "title": "Programming PHP", 756 | "author": "Rasmus Lerdorf" 757 | }, 758 | { 759 | "isbn": "1565926811", 760 | "title": "PHP cookbook", 761 | "author": "David Sklar" 762 | }, 763 | { 764 | "isbn": "0596006365", 765 | "title": "Upgrading to PHP 5", 766 | "author": "Adam Trachtenberg" 767 | }, 768 | { 769 | "isbn": "0674387260", 770 | "title": "The hellenistic world", 771 | "author": "F. W. Walbank" 772 | }, 773 | { 774 | "isbn": "0195065883", 775 | "title": "The Western way of war : infantry battle in classical Greece", 776 | "author": "Victor Davis Hanson" 777 | }, 778 | { 779 | "isbn": "0140132678", 780 | "title": "The life of Andrew Jackson", 781 | "author": "Robert Vincent Remini" 782 | }, 783 | { 784 | "isbn": "0198219156", 785 | "title": "The Legacy of Greece : a new appraisal", 786 | "author": "M. I. Finley" 787 | }, 788 | { 789 | "isbn": "0030785006", 790 | "title": "The nature of historical inquiry. Edited by Leonard M. Marsak", 791 | "author": "Leonard Mendes Marsak" 792 | }, 793 | { 794 | "isbn": "014013686X", 795 | "title": "The world of Odysseus [by] M.I. Finley", 796 | "author": "M. I. Finley" 797 | }, 798 | { 799 | "isbn": "0801486335", 800 | "title": "Writing ancient history", 801 | "author": "Neville Morley" 802 | }, 803 | { 804 | "isbn": "0140552251", 805 | "title": "The use and abuse of history", 806 | "author": "M. I. Finley" 807 | }, 808 | { 809 | "isbn": "0520024478", 810 | "title": "Researching and writing in history; a practical handbook for students, by F. N. McCoy", 811 | "author": "Florence N. McCoy" 812 | }, 813 | { 814 | "isbn": "0806119381", 815 | "title": "The historians of Greece and Rome", 816 | "author": "Stephen Usher" 817 | }, 818 | { 819 | "isbn": "0801411165", 820 | "title": "Knowledge and explanation in history : an introduction to the philosophy of history", 821 | "author": "R. F. Atkinson" 822 | }, 823 | { 824 | "isbn": "0684844451", 825 | "title": "The killing of history : how literary critics and social theorists are murdering our past", 826 | "author": "Keith Windschuttle" 827 | }, 828 | { 829 | "isbn": "0393046877", 830 | "title": "In defense of history", 831 | "author": "Richard J. Evans" 832 | }, 833 | { 834 | "isbn": "0807842621", 835 | "title": "Women and law in classical Greece", 836 | "author": "Raphael Sealey" 837 | }, 838 | { 839 | "isbn": "0807823090", 840 | "title": "News and society in the Greek polis", 841 | "author": "Sian Lewis" 842 | }, 843 | { 844 | "isbn": "0226072789", 845 | "title": "Historiography : ancient, medieval & modern", 846 | "author": "Ernst Breisach" 847 | }, 848 | { 849 | "isbn": "0674615808", 850 | "title": "The new history and the old", 851 | "author": "Gertrude Himmelfarb" 852 | }, 853 | { 854 | "isbn": "0764555898", 855 | "title": "PHP and MySQL for dummies", 856 | "author": "Janet Valade" 857 | }, 858 | { 859 | "isbn": "0521406730", 860 | "title": "Politics in the ancient world", 861 | "author": "M. I. Finley" 862 | }, 863 | { 864 | "isbn": "0879518987", 865 | "title": "Turkey unveiled : a history of modern Turkey", 866 | "author": "Nicole Pope" 867 | }, 868 | { 869 | "isbn": "0688088058", 870 | "title": "The hole in the flag : a Romanian exile's story of return and revolution", 871 | "author": "Andrei Codrescu" 872 | }, 873 | { 874 | "isbn": "0738209015", 875 | "title": "The last best league : one summer, one season, one dream", 876 | "author": "Jim Collins" 877 | }, 878 | { 879 | "isbn": "0679759239", 880 | "title": "On looking into the Abyss : untimely thoughts on culture and society", 881 | "author": "Gertrude Himmelfarb" 882 | }, 883 | { 884 | "isbn": "039545185X", 885 | "title": "Landslide : the unmaking of the President, 1984-1988", 886 | "author": "Jane Mayer" 887 | }, 888 | { 889 | "isbn": "0192852949", 890 | "title": "The Oxford illustrated history of the Crusades", 891 | "author": "Jonathan Simon Christopher Riley-Smith" 892 | }, 893 | { 894 | "isbn": "0385504713", 895 | "title": "Reagan's war : the epic story of his forty year struggle and final triumph over communism", 896 | "author": "Peter Schweizer" 897 | }, 898 | { 899 | "isbn": "0715631942", 900 | "title": "The oracles of the ancient world : a complete guide", 901 | "author": "Trevor Curnow" 902 | }, 903 | { 904 | "isbn": "0821211080", 905 | "title": "The Search for Alexander : an exhibition", 906 | "author": "Nikolaos Gialoures" 907 | }, 908 | { 909 | "isbn": "0520074319", 910 | "title": "Reading the past : ancient writing from cuneiform to the alphabet", 911 | "author": "J. T Hooker" 912 | }, 913 | { 914 | "isbn": "0688080936", 915 | "title": "The Ottoman centuries : the rise and fall of the Turkish Empire", 916 | "author": "Patrick Balfour Kinross, Baron" 917 | }, 918 | { 919 | "isbn": "006621288X", 920 | "title": "Love in the asylum : a novel", 921 | "author": "Lisa Carey" 922 | }, 923 | { 924 | "isbn": "0380976749", 925 | "title": "The mermaids singing", 926 | "author": "Lisa Carey" 927 | }, 928 | { 929 | "isbn": "075370000X", 930 | "title": "Persian Mythology", 931 | "author": "John Hinnells" 932 | }, 933 | { 934 | "isbn": "0596006179", 935 | "title": "Learning UNIX for Mac OS X Panther", 936 | "author": "Dave Taylor" 937 | }, 938 | { 939 | "isbn": "0674404254", 940 | "title": "Hitlers World View : A Blueprint for Power", 941 | "author": "Herbert Arnold" 942 | }, 943 | { 944 | "isbn": "0674991753", 945 | "title": "The histories, with an English translation by W. R. Paton. In six volumes", 946 | "author": "Polybius" 947 | }, 948 | { 949 | "isbn": "0140440399", 950 | "title": "Thucydides, with an English translation by C. Forster Smith ... in four volumes ... History of the", 951 | "author": "Thucydides" 952 | }, 953 | { 954 | "isbn": "067499535X", 955 | "title": "Historical miscellany", 956 | "author": "3rd cent Aelian" 957 | }, 958 | { 959 | "isbn": "0521306965", 960 | "title": "A Hellenistic anthology", 961 | "author": "Neil Hopkinson" 962 | }, 963 | { 964 | "isbn": "0385248326", 965 | "title": "The New Jerusalem Bible : reader's edition", 966 | "author": "" 967 | }, 968 | { 969 | "isbn": "0879737018", 970 | "title": "Relics", 971 | "author": "Joan Carroll Cruz" 972 | }, 973 | { 974 | "isbn": "069108923X", 975 | "title": "One true God : historical consequences of monotheism", 976 | "author": "Rodney Stark" 977 | }, 978 | { 979 | "isbn": "0060633980", 980 | "title": "What makes us Catholic : eight gifts for life", 981 | "author": "Thomas H. Groome" 982 | }, 983 | { 984 | "isbn": "0618134298", 985 | "title": "Why I am a Catholic", 986 | "author": "Garry Wills" 987 | }, 988 | { 989 | "isbn": "0912141824", 990 | "title": "Wrath of God: The days of the Antichrist", 991 | "author": "Livio Fanzaga" 992 | }, 993 | { 994 | "isbn": "0393004759", 995 | "title": "Pagan Mysteries in the Renaissance.", 996 | "author": "Edgar Wind" 997 | }, 998 | { 999 | "isbn": "0679450319", 1000 | "title": "The kidnapping of Edgardo Mortara", 1001 | "author": "David I. Kertzer" 1002 | }, 1003 | { 1004 | "isbn": "051732170X", 1005 | "title": "Who's who in the Old Testament / Who's who in the Bible : two volumes in one", 1006 | "author": "Joan Comay" 1007 | }, 1008 | { 1009 | "isbn": "0674932609", 1010 | "title": "Inside the Vatican : the politics and organization of the Catholic Church", 1011 | "author": "Thomas J. Reese" 1012 | }, 1013 | { 1014 | "isbn": "0395709369", 1015 | "title": "Women in scripture : a dictionary of named and unnamed women in the Hebrew Bible, the", 1016 | "author": "Carol L. Meyers" 1017 | }, 1018 | { 1019 | "isbn": "067483996X", 1020 | "title": "Strategy : the logic of war and peace", 1021 | "author": "Edward Luttwak" 1022 | }, 1023 | { 1024 | "isbn": "0750914378", 1025 | "title": "Jacobite Spy Wars", 1026 | "author": "Hugh Douglas" 1027 | }, 1028 | { 1029 | "isbn": "156619430X", 1030 | "title": "Sparta [by] A. H. M. Jones", 1031 | "author": "A. H. M. Jones" 1032 | }, 1033 | { 1034 | "isbn": "1578517087", 1035 | "title": "The social life of information", 1036 | "author": "John Seely Brown" 1037 | }, 1038 | { 1039 | "isbn": "0674541510", 1040 | "title": "Magic in the ancient world", 1041 | "author": "Fritz Graf" 1042 | }, 1043 | { 1044 | "isbn": "0596001320", 1045 | "title": "Learning Perl", 1046 | "author": "Randal L. Schwartz" 1047 | }, 1048 | { 1049 | "isbn": "0764506927", 1050 | "title": "XML for dummies", 1051 | "author": "Ed Tittel" 1052 | }, 1053 | { 1054 | "isbn": "067232525X", 1055 | "title": "PHP and MySQL Web development", 1056 | "author": "Luke Welling" 1057 | }, 1058 | { 1059 | "isbn": "0520031830", 1060 | "title": "Latin: An Intensive Course", 1061 | "author": "Rita Fleischer" 1062 | }, 1063 | { 1064 | "isbn": "0500276161", 1065 | "title": "In search of the Indo-Europeans : language, archaeology, and myth", 1066 | "author": "J. P. Mallory" 1067 | }, 1068 | { 1069 | "isbn": "0520231929", 1070 | "title": "In the Footsteps of Alexander The Great: A Journey from Greece to Asia", 1071 | "author": "Michael Wood" 1072 | }, 1073 | { 1074 | "isbn": "0394491130", 1075 | "title": "The nature of Alexander", 1076 | "author": "Mary Renault" 1077 | }, 1078 | { 1079 | "isbn": "091514820X", 1080 | "title": "The road to Eleusis : unveiling the secret of the mysteries", 1081 | "author": "R. Gordon Wasson" 1082 | }, 1083 | { 1084 | "isbn": "0714120782", 1085 | "title": "Mesopotamia", 1086 | "author": "Julian Reade" 1087 | }, 1088 | { 1089 | "isbn": "0192852485", 1090 | "title": "The Oxford history of the Roman world", 1091 | "author": "John Boardman" 1092 | }, 1093 | { 1094 | "isbn": "1401902405", 1095 | "title": "How to ruin your love life", 1096 | "author": "Benjamin Stein" 1097 | }, 1098 | { 1099 | "isbn": "0393318648", 1100 | "title": "Shackleton's Boat Journey", 1101 | "author": "Frank Arthur Worsley" 1102 | }, 1103 | { 1104 | "isbn": "0486277844", 1105 | "title": "Selected poems", 1106 | "author": "George Gordon Byron Byron, Baron" 1107 | }, 1108 | { 1109 | "isbn": "0385501145", 1110 | "title": "The songs of the kings", 1111 | "author": "Barry Unsworth" 1112 | }, 1113 | { 1114 | "isbn": "0380976757", 1115 | "title": "In the country of the young", 1116 | "author": "Lisa Carey" 1117 | }, 1118 | { 1119 | "isbn": "0819151505", 1120 | "title": "The poems of Catullus : a teaching text", 1121 | "author": "Phyllis Young Forsyth" 1122 | }, 1123 | { 1124 | "isbn": "053111693X", 1125 | "title": "Slavery in ancient Greece and Rome", 1126 | "author": "Jacqueline Dembar Greene" 1127 | }, 1128 | { 1129 | "isbn": "0674543203", 1130 | "title": "The making of late antiquity", 1131 | "author": "Peter Robert Lamont Brown" 1132 | }, 1133 | { 1134 | "isbn": "0596000359", 1135 | "title": "Information architecture for the World Wide Web", 1136 | "author": "Louis Rosenfeld" 1137 | }, 1138 | { 1139 | "isbn": "0195087070", 1140 | "title": "The Oxford history of the biblical world", 1141 | "author": "Michael David Coogan" 1142 | }, 1143 | { 1144 | "isbn": "0195046455", 1145 | "title": "The Oxford companion to the Bible", 1146 | "author": "Bruce Manning Metzger" 1147 | }, 1148 | { 1149 | "isbn": "0805040811", 1150 | "title": "Lords of the horizons : a history of the Ottoman Empire", 1151 | "author": "Jason Goodwin" 1152 | }, 1153 | { 1154 | "isbn": "037571457X", 1155 | "title": "Persepolis: The Story of a Childhood", 1156 | "author": "Marjane Satrapi" 1157 | }, 1158 | { 1159 | "isbn": "0395952832", 1160 | "title": "Sheba : through the desert in search of the legendary queen", 1161 | "author": "Nicholas Clapp" 1162 | }, 1163 | { 1164 | "isbn": "048626551X", 1165 | "title": "The Napoleon of Notting Hill", 1166 | "author": "G. K. Chesterton" 1167 | }, 1168 | { 1169 | "isbn": "0061315451", 1170 | "title": "Historians' Fallacies : Toward a Logic of Historical Thought", 1171 | "author": "David H. Fischer" 1172 | }, 1173 | { 1174 | "isbn": "1874166382", 1175 | "title": "Introducing Derrida", 1176 | "author": "Jeff Collins" 1177 | }, 1178 | { 1179 | "isbn": "0394580486", 1180 | "title": "The Romanovs : the final chapter", 1181 | "author": "Robert K. Massie" 1182 | }, 1183 | { 1184 | "isbn": "0596002114", 1185 | "title": "Managing and Using MySQL (2nd Edition)", 1186 | "author": "George Reese" 1187 | }, 1188 | { 1189 | "isbn": "0596003064", 1190 | "title": "High performance MySQL : optimization, backups, replication, and load balancing", 1191 | "author": "Jeremy D. Zawodny" 1192 | }, 1193 | { 1194 | "isbn": "0156495899", 1195 | "title": "Lectures on literature", 1196 | "author": "Vladimir Vladimirovich Nabokov" 1197 | }, 1198 | { 1199 | "isbn": "0156495910", 1200 | "title": "Lectures on Russian Literature", 1201 | "author": "Vladimir Nabokov" 1202 | }, 1203 | { 1204 | "isbn": "0060953497", 1205 | "title": "The Keys of Egypt: The Race to Crack the Hieroglyph Code", 1206 | "author": "Lesley Adkins" 1207 | }, 1208 | { 1209 | "isbn": "0060937319", 1210 | "title": "A People's History of the United States: 1492 to Present", 1211 | "author": "Howard Zinn" 1212 | }, 1213 | { 1214 | "isbn": "141162551X", 1215 | "title": "The Web Developer's Guide to Amazon E-commerce Service: Developing Web Applications Using Amazon Web Services And Php", 1216 | "author": "Jason Levitt" 1217 | }, 1218 | { 1219 | "isbn": "0749306459", 1220 | "title": "Tiger! Tiger!", 1221 | "author": "Alfred Bester" 1222 | }, 1223 | { 1224 | "isbn": "0716744732", 1225 | "title": "The power of Babel : a natural history of language", 1226 | "author": "John H. McWhorter" 1227 | }, 1228 | { 1229 | "isbn": "0140167935", 1230 | "title": "The Manticore (Deptford Trilogy)", 1231 | "author": "Robertson Davies" 1232 | }, 1233 | { 1234 | "isbn": "074323491X", 1235 | "title": "Perelandra (Space Trilogy (Paperback))", 1236 | "author": "C.S. Lewis" 1237 | }, 1238 | { 1239 | "isbn": "1585674028", 1240 | "title": "The Spartans", 1241 | "author": "Paul Cartledge" 1242 | }, 1243 | { 1244 | "isbn": "1413302157", 1245 | "title": "Whoops! I'm In Business: A Crash Course In Business Basics", 1246 | "author": "Richard Stim" 1247 | }, 1248 | { 1249 | "isbn": "0764552775", 1250 | "title": "Public Relations Kit for Dummies", 1251 | "author": "Eric Yaverbaum" 1252 | }, 1253 | { 1254 | "isbn": "0764550934", 1255 | "title": "Small Business Kit for Dummies", 1256 | "author": "Richard D. Harroch" 1257 | }, 1258 | { 1259 | "isbn": "0672326736", 1260 | "title": "MySQL (3rd Edition) (Developer's Library)", 1261 | "author": "Paul DuBois" 1262 | }, 1263 | { 1264 | "isbn": "0764541471", 1265 | "title": "UNIX for Dummies", 1266 | "author": "John R. Levine" 1267 | }, 1268 | { 1269 | "isbn": "0553562614", 1270 | "title": "Snow crash", 1271 | "author": "Neal Stephenson" 1272 | }, 1273 | { 1274 | "isbn": "014016796X", 1275 | "title": "World of wonders", 1276 | "author": "Robertson Davies" 1277 | }, 1278 | { 1279 | "isbn": "0553804022", 1280 | "title": "No True Glory : A Frontline Account of the Battle for Fallujah", 1281 | "author": "Bing West" 1282 | }, 1283 | { 1284 | "isbn": "1401302378", 1285 | "title": "The Long Tail: Why the Future of Business Is Selling Less of More", 1286 | "author": "Chris Anderson" 1287 | }, 1288 | { 1289 | "isbn": "1893329186", 1290 | "title": "Hungry? Boston: The Lowdown on Where the Real People Eat!", 1291 | "author": "Esti Iturralde" 1292 | }, 1293 | { 1294 | "isbn": "0299198006", 1295 | "title": "Legendary Ireland : Journey Through Celtic Places and Myths", 1296 | "author": "Eithne Massey" 1297 | }, 1298 | { 1299 | "isbn": "0761120149", 1300 | "title": "How to Grill: The Complete Illustrated Book of Barbecue Techniques", 1301 | "author": "Steven Raichlen" 1302 | }, 1303 | { 1304 | "isbn": "1881957322", 1305 | "title": "Macperl: Power and Ease", 1306 | "author": "Vicki Brown" 1307 | }, 1308 | { 1309 | "isbn": "1565924878", 1310 | "title": "Java in a Nutshell : A Desktop Quick Reference (Java Series) (3rd Edition)", 1311 | "author": "David Flanagan" 1312 | }, 1313 | { 1314 | "isbn": "0735712859", 1315 | "title": "Inside JavaScript", 1316 | "author": "Steven Holzner" 1317 | }, 1318 | { 1319 | "isbn": "1565924789", 1320 | "title": "Programming Web Graphics with Perl & GNU Software (O'Reilly Nutshell)", 1321 | "author": "Shawn Wallace" 1322 | }, 1323 | { 1324 | "isbn": "1595940146", 1325 | "title": "Your Safari Dragons: In Search of the Real Komodo Dragon", 1326 | "author": "Daniel White" 1327 | }, 1328 | { 1329 | "isbn": "0596002890", 1330 | "title": "Mastering Regular Expressions, Second Edition", 1331 | "author": "Jeffrey E F Friedl" 1332 | }, 1333 | { 1334 | "isbn": "0253325552", 1335 | "title": "Turkish traditional art today", 1336 | "author": "Henry H. Glassie" 1337 | }, 1338 | { 1339 | "isbn": "0395177111", 1340 | "title": "The Hobbit", 1341 | "author": "J.R.R. Tolkien" 1342 | }, 1343 | { 1344 | "isbn": "0151957479", 1345 | "title": "The Western Canon: The Books and School of the Ages", 1346 | "author": "Harold Bloom" 1347 | }, 1348 | { 1349 | "isbn": "395177111", 1350 | "title": "The hobbit, or, There and back again", 1351 | "author": "J. R. R. Tolkien" 1352 | }, 1353 | { 1354 | "isbn": "034542204X", 1355 | "title": "Tales of the Cthulhu mythos", 1356 | "author": "H. P. Lovecraft" 1357 | }, 1358 | { 1359 | "isbn": "0375727345", 1360 | "title": "House of Sand and Fog (Oprah's Book Club)", 1361 | "author": "Andre Dubus III" 1362 | }, 1363 | { 1364 | "isbn": "0246974710", 1365 | "title": "The poems of Catullus;", 1366 | "author": "Gaius Valerius Catullus" 1367 | }, 1368 | { 1369 | "isbn": "0671005073", 1370 | "title": "Plays of Sophocles", 1371 | "author": "William Walter" 1372 | }, 1373 | { 1374 | "isbn": "0486295060", 1375 | "title": "The war of the worlds", 1376 | "author": "H. G. Wells" 1377 | }, 1378 | { 1379 | "isbn": "0684844532", 1380 | "title": "Who killed Homer? : the demise of classical education and the recovery of Greek wisdom", 1381 | "author": "Victor Davis Hanson" 1382 | }, 1383 | { 1384 | "isbn": "0786866594", 1385 | "title": "Wings of madness : Alberto Santos-Dumont and the invention of flight", 1386 | "author": "Paul Hoffman" 1387 | }, 1388 | { 1389 | "isbn": "0715612581", 1390 | "title": "Primer of Greek Grammar", 1391 | "author": "Evelyn Abbott" 1392 | }, 1393 | { 1394 | "isbn": "0064601048", 1395 | "title": "Latin an Introductory Course", 1396 | "author": "Frederic M Wheelock" 1397 | }, 1398 | { 1399 | "isbn": "0715614703", 1400 | "title": "Latin phrase-book", 1401 | "author": "Carl Meissner" 1402 | }, 1403 | { 1404 | "isbn": "0817303502", 1405 | "title": "A Latin grammar", 1406 | "author": "William Gardner Hale" 1407 | }, 1408 | { 1409 | "isbn": "0140443185", 1410 | "title": "Rome and the Mediterranean : books XXXI-XLV of The history of Rome from its foundation", 1411 | "author": "Livy" 1412 | }, 1413 | { 1414 | "isbn": "0140443622", 1415 | "title": "The rise of the Roman Empire", 1416 | "author": "Polybius" 1417 | }, 1418 | { 1419 | "isbn": "0140440070", 1420 | "title": "The Persian expedition", 1421 | "author": "Xenophon" 1422 | }, 1423 | { 1424 | "isbn": "0140443088", 1425 | "title": "Lives of the later Caesars : the first part of the Augustan history : with newly compiled Lives of Nerva and Trajan", 1426 | "author": "" 1427 | }, 1428 | { 1429 | "isbn": "0140441743", 1430 | "title": "Thyestes; Phaedra; The Trojan women; Oedipus; with, Octavia;", 1431 | "author": "Lucius Annaeus Seneca" 1432 | }, 1433 | { 1434 | "isbn": "0140440720", 1435 | "title": "The twelve Caesars", 1436 | "author": "Suetonius" 1437 | }, 1438 | { 1439 | "isbn": "0140442790", 1440 | "title": "The satires of Horace and Persius;", 1441 | "author": "Horace" 1442 | }, 1443 | { 1444 | "isbn": "1892284901", 1445 | "title": "Night Moves and Other Stories", 1446 | "author": "Tim Powers" 1447 | }, 1448 | { 1449 | "isbn": "0140444637", 1450 | "title": "Plutarch on Sparta (Penguin Classics)", 1451 | "author": "Plutarch" 1452 | }, 1453 | { 1454 | "isbn": "0226469409", 1455 | "title": "The Iliad", 1456 | "author": "Homer" 1457 | }, 1458 | { 1459 | "isbn": "0345440919", 1460 | "title": "Pavane", 1461 | "author": "Keith Roberts" 1462 | }, 1463 | { 1464 | "isbn": "0140444254", 1465 | "title": "The three Theban plays", 1466 | "author": "Sophocles" 1467 | }, 1468 | { 1469 | "isbn": "0140441328", 1470 | "title": "The Jugurthine War & The Conspiracy of Catiline", 1471 | "author": "Sallust" 1472 | }, 1473 | { 1474 | "isbn": "0140445234", 1475 | "title": "The Idylls", 1476 | "author": "Theocritus" 1477 | }, 1478 | { 1479 | "isbn": "0460871528", 1480 | "title": "The republic", 1481 | "author": "Plato" 1482 | }, 1483 | { 1484 | "isbn": "0140440488", 1485 | "title": "The Republic", 1486 | "author": "Plato" 1487 | }, 1488 | { 1489 | "isbn": "0192831208", 1490 | "title": "Seven commentaries on the Gallic war", 1491 | "author": "Julius Caesar" 1492 | }, 1493 | { 1494 | "isbn": "0140441492", 1495 | "title": "The Pot of Gold and Other Plays (Classics S.)", 1496 | "author": "Plautus" 1497 | }, 1498 | { 1499 | "isbn": "0140440585", 1500 | "title": "Metamorphoses (Penguin Classics ed.)", 1501 | "author": "Ovid" 1502 | }, 1503 | { 1504 | "isbn": "0140441948", 1505 | "title": "Sixteen Satires (Classics S.)", 1506 | "author": "Juvenal" 1507 | }, 1508 | { 1509 | "isbn": "0140440038", 1510 | "title": "The Theban plays", 1511 | "author": "Sophocles" 1512 | }, 1513 | { 1514 | "isbn": "0140441166", 1515 | "title": "Les liaisons dangereuses", 1516 | "author": "Choderlos de Laclos" 1517 | }, 1518 | { 1519 | "isbn": "1853260258", 1520 | "title": "The Odyssey (Wordsworth Classics)", 1521 | "author": "Homer" 1522 | }, 1523 | { 1524 | "isbn": "0140621318", 1525 | "title": "The Prisoner of Zenda (Penguin Popular Classics)", 1526 | "author": "Anthony Hope" 1527 | }, 1528 | { 1529 | "isbn": "1853266175", 1530 | "title": "Priapeia", 1531 | "author": "L C Smithers" 1532 | }, 1533 | { 1534 | "isbn": "0198140983", 1535 | "title": "The Oxford history of Byzantium", 1536 | "author": "" 1537 | }, 1538 | { 1539 | "isbn": "0691008566", 1540 | "title": "Islamic history : a framework for inquiry", 1541 | "author": "R. Stephen Humphreys" 1542 | }, 1543 | { 1544 | "isbn": "0393046729", 1545 | "title": "A rage to live : a biography of Richard and Isabel Burton", 1546 | "author": "Mary S. Lovell" 1547 | }, 1548 | { 1549 | "isbn": "0812925750", 1550 | "title": "A struggle for power : the American revolution", 1551 | "author": "Theodore Draper" 1552 | }, 1553 | { 1554 | "isbn": "0691017549", 1555 | "title": "The Muqaddimah, an introduction to history", 1556 | "author": "Ibn Khaldˆun" 1557 | }, 1558 | { 1559 | "isbn": "0312239246", 1560 | "title": "The Sappho companion", 1561 | "author": "" 1562 | }, 1563 | { 1564 | "isbn": "0679311017", 1565 | "title": "Spoken here : travels among threatened languages", 1566 | "author": "Mark Abley" 1567 | }, 1568 | { 1569 | "isbn": "0195144201", 1570 | "title": "What went wrong? : Western impact and Middle Eastern response", 1571 | "author": "Bernard Lewis" 1572 | }, 1573 | { 1574 | "isbn": "0801859557", 1575 | "title": "The Portuguese empire, 1415-1808 : a world on the move", 1576 | "author": "A. J. R. Russell-Wood" 1577 | }, 1578 | { 1579 | "isbn": "0618127380", 1580 | "title": "The Italian Renaissance", 1581 | "author": "J. H. Plumb" 1582 | }, 1583 | { 1584 | "isbn": "0140077022", 1585 | "title": "White noise", 1586 | "author": "Don DeLillo" 1587 | }, 1588 | { 1589 | "isbn": "0813511984", 1590 | "title": "History of the Byzantine State", 1591 | "author": "Georgije Ostrogorski" 1592 | }, 1593 | { 1594 | "isbn": "0486255948", 1595 | "title": "Treasury of Turkish designs : 670 motifs from Iznik pottery", 1596 | "author": "Azade Akar" 1597 | }, 1598 | { 1599 | "isbn": "0151007098", 1600 | "title": "The object-lesson", 1601 | "author": "Edward Gorey" 1602 | }, 1603 | { 1604 | "isbn": "0140442367", 1605 | "title": "The pillow book of Sei Shonagon", 1606 | "author": "Sei Shonagon" 1607 | }, 1608 | { 1609 | "isbn": "0486426858", 1610 | "title": "The Master of Ballantrae", 1611 | "author": "Robert Louis Stevenson" 1612 | }, 1613 | { 1614 | "isbn": "0385418957", 1615 | "title": "Seven Pillars of Wisdom", 1616 | "author": "T. E. Lawrence" 1617 | }, 1618 | { 1619 | "isbn": "0460877992", 1620 | "title": "Robert Herrick (Everyman Poetry Library)", 1621 | "author": "Robert Herrick" 1622 | }, 1623 | { 1624 | "isbn": "1565921496", 1625 | "title": "Programming Perl", 1626 | "author": "Larry Wall" 1627 | }, 1628 | { 1629 | "isbn": "1400049636", 1630 | "title": "There Must Be a Pony in Here Somewhere: The AOL Time Warner Debacle and the Quest for a Digital Future", 1631 | "author": "Kara Swisher" 1632 | }, 1633 | { 1634 | "isbn": "0812690699", 1635 | "title": "The machinery of freedom : guide to a radical capitalism", 1636 | "author": "David D. Friedman" 1637 | }, 1638 | { 1639 | "isbn": "047206035X", 1640 | "title": "The Life of Charlemagne (Ann Arbor Paperbacks)", 1641 | "author": "Einhard" 1642 | }, 1643 | { 1644 | "isbn": "0060392339", 1645 | "title": "Jen-X : Jenny McCarthy's open book", 1646 | "author": "Jenny McCarthy" 1647 | }, 1648 | { 1649 | "isbn": "0674471741", 1650 | "title": "Jane Austen", 1651 | "author": "Tony Tanner" 1652 | }, 1653 | { 1654 | "isbn": "067974102X", 1655 | "title": "My Life as Author and Editor", 1656 | "author": "H.L. Mencken" 1657 | }, 1658 | { 1659 | "isbn": "0809437228", 1660 | "title": "In Defense of Women (Time Reading Program Special Edition)", 1661 | "author": "H. L. Mencken" 1662 | }, 1663 | { 1664 | "isbn": "0460110977", 1665 | "title": "Mabinogian (2nd Edition)", 1666 | "author": "Jones" 1667 | }, 1668 | { 1669 | "isbn": "0195055373", 1670 | "title": "Revolutionary Dreams: Utopian Vision and Experimental Life in the Russian Revolution", 1671 | "author": "Richard Stites" 1672 | }, 1673 | { 1674 | "isbn": "0811200124", 1675 | "title": "Labyrinths: Selected Stories and Other Writings (New Directions Paperbook, 186)", 1676 | "author": "Jorge Luis Borges" 1677 | }, 1678 | { 1679 | "isbn": "0140440267", 1680 | "title": "The Lusiads", 1681 | "author": "Luís de Camões" 1682 | }, 1683 | { 1684 | "isbn": "0140442405", 1685 | "title": "Praise of folly; and, Letter to Martin Dorp, 1515", 1686 | "author": "Desiderius Erasmus" 1687 | }, 1688 | { 1689 | "isbn": "0674690052", 1690 | "title": "Population, Disease, and Land in Early Japan, 645-900 (Harvard-Yenching Institute Monograph Series)", 1691 | "author": "William Wayne Farris" 1692 | }, 1693 | { 1694 | "isbn": "0028461703", 1695 | "title": "MORAL AND POLITICAL PHILOSOPHY (Hafner Library of Classics)", 1696 | "author": "David Hume" 1697 | }, 1698 | { 1699 | "isbn": "0140047840", 1700 | "title": "Nabokov, his life in part", 1701 | "author": "Andrew Field" 1702 | }, 1703 | { 1704 | "isbn": "0801491398", 1705 | "title": "Medieval political philosophy : a sourcebook", 1706 | "author": "Ralph Lerner" 1707 | }, 1708 | { 1709 | "isbn": "0449906043", 1710 | "title": "Karma Cola", 1711 | "author": "Gita Mehta" 1712 | }, 1713 | { 1714 | "isbn": "0060916400", 1715 | "title": "The other path : the invisible revolution in the Third World", 1716 | "author": "Hernando de Soto" 1717 | }, 1718 | { 1719 | "isbn": "0395735297", 1720 | "title": "War As I Knew It", 1721 | "author": "George S. Patton" 1722 | }, 1723 | { 1724 | "isbn": "0140441832", 1725 | "title": "King Harald's Saga: Harald Hardradi of Norway: From Snorri Sturluson's Heimskringla (Classics S.)", 1726 | "author": "Snorri Sturluson" 1727 | }, 1728 | { 1729 | "isbn": "0060914033", 1730 | "title": "The ides of March", 1731 | "author": "Thornton Wilder" 1732 | }, 1733 | { 1734 | "isbn": "0452264510", 1735 | "title": "The Prime of Miss Jean Brodie", 1736 | "author": "Muriel Spark" 1737 | }, 1738 | { 1739 | "isbn": "0451527631", 1740 | "title": "Looking backward : 2000-1887", 1741 | "author": "Edward Bellamy" 1742 | }, 1743 | { 1744 | "isbn": "039417299X", 1745 | "title": "Three Novels by Samuel Beckett: Molloy Malone Dies the Unnamable", 1746 | "author": "Samuel Beckett" 1747 | }, 1748 | { 1749 | "isbn": "043590003X", 1750 | "title": "No longer at ease", 1751 | "author": "Chinua Achebe" 1752 | }, 1753 | { 1754 | "isbn": "0449239195", 1755 | "title": "Things fall apart", 1756 | "author": "Chinua Achebe" 1757 | }, 1758 | { 1759 | "isbn": "0226468046", 1760 | "title": "Women, Fire, and Dangerous Things", 1761 | "author": "George Lakoff" 1762 | }, 1763 | { 1764 | "isbn": "0262522950", 1765 | "title": "Sorting Things Out: Classification and Its Consequences (Inside Technology)", 1766 | "author": "Geoffrey C. Bowker" 1767 | }, 1768 | { 1769 | "isbn": "0233972935", 1770 | "title": "Ordering the world : a history of classifying man", 1771 | "author": "David M. Knight" 1772 | }, 1773 | { 1774 | "isbn": "0425129616", 1775 | "title": "The Mysterious Affair at Styles", 1776 | "author": "Agatha Christie" 1777 | }, 1778 | { 1779 | "isbn": "0385500998", 1780 | "title": "The virtues of war : a novel of Alexander the Great", 1781 | "author": "Steven Pressfield" 1782 | }, 1783 | { 1784 | "isbn": "0140189866", 1785 | "title": "Species of Spaces and Other Pieces (Twentieth Century Classics)", 1786 | "author": "Georges Perec" 1787 | }, 1788 | { 1789 | "isbn": "0915179016", 1790 | "title": "How to start your own country : how you can profit from the coming decline of the nation state", 1791 | "author": "Erwin S. Strauss" 1792 | }, 1793 | { 1794 | "isbn": "0316164933", 1795 | "title": "The Perfect Store: Inside eBay", 1796 | "author": "Adam Cohen" 1797 | }, 1798 | { 1799 | "isbn": "0385489765", 1800 | "title": "High-Tech Heretic: Reflections of a Computer Contrarian", 1801 | "author": "Clifford Stoll" 1802 | }, 1803 | { 1804 | "isbn": "0781803950", 1805 | "title": "Treasury of Arabic Love: Poems, Quotations & Proverbs in Arabic and English", 1806 | "author": "" 1807 | }, 1808 | { 1809 | "isbn": "0198148291", 1810 | "title": "A Historical Commentary on Arrian's History of Alexander: Volume II: Commentary on Books IV-V", 1811 | "author": "A. B. Bosworth" 1812 | }, 1813 | { 1814 | "isbn": "0399239545", 1815 | "title": "Slowly, Slowly, Slowly Said The Sloth", 1816 | "author": "" 1817 | }, 1818 | { 1819 | "isbn": "0805079769", 1820 | "title": "Hidden Iran : paradox and power in the Islamic Republic", 1821 | "author": "Ray Takeyh" 1822 | }, 1823 | { 1824 | "isbn": "0226458075", 1825 | "title": "The Structure of Scientific Revolutions", 1826 | "author": "Thomas S. Kuhn" 1827 | }, 1828 | { 1829 | "isbn": "0345421949", 1830 | "title": "Roads not taken : tales of alternate history", 1831 | "author": "Stanley Schmidt" 1832 | }, 1833 | { 1834 | "isbn": "047009608X", 1835 | "title": "Second Life: The Official Guide", 1836 | "author": "Michael Rymaszewski" 1837 | }, 1838 | { 1839 | "isbn": "9789078779018", 1840 | "title": "Risen - Why libraries are here to stay", 1841 | "author": "Bastiaan F. Zwaan" 1842 | }, 1843 | { 1844 | "isbn": "0596529325", 1845 | "title": "Programming Collective Intelligence: Building Smart Web 2.0 Applications", 1846 | "author": "Toby Segaran" 1847 | }, 1848 | { 1849 | "isbn": "0674018621", 1850 | "title": "The highly civilized man : Richard Burton and the Victorian world", 1851 | "author": "Dane Keith. Kennedy" 1852 | }, 1853 | { 1854 | "isbn": "0321529170", 1855 | "title": "Tagging: People-powered Metadata for the Social Web (Voices That Matter)", 1856 | "author": "Gene Smith" 1857 | }, 1858 | { 1859 | "isbn": "0596002815", 1860 | "title": "Learning Python", 1861 | "author": "Mark Lutz" 1862 | }, 1863 | { 1864 | "isbn": "0691058873", 1865 | "title": "The Horse, the Wheel, and Language: How Bronze-Age Riders from the Eurasian Steppes Shaped the Modern World", 1866 | "author": "David W. Anthony" 1867 | }, 1868 | { 1869 | "isbn": "1594201536", 1870 | "title": "Here Comes Everybody: The Power of Organizing Without Organizations", 1871 | "author": "Clay Shirky" 1872 | }, 1873 | { 1874 | "isbn": "0674013859", 1875 | "title": "Historical atlas of Islam", 1876 | "author": "Malise Ruthven" 1877 | }, 1878 | { 1879 | "isbn": "9780199262137", 1880 | "title": "War in human civilization", 1881 | "author": "Azar Gat" 1882 | }, 1883 | { 1884 | "isbn": "158234518X", 1885 | "title": "Zeus: A Journey Through Greece in the Footsteps of a God", 1886 | "author": "Tom Stone" 1887 | }, 1888 | { 1889 | "isbn": "0500051410", 1890 | "title": "The Leopard's Tale: Revealing the Mysteries of Catalhoyuk", 1891 | "author": "Ian Hodder" 1892 | }, 1893 | { 1894 | "isbn": "0394800095", 1895 | "title": "The Whales Go By", 1896 | "author": "Phleger" 1897 | }, 1898 | { 1899 | "isbn": "0393064778", 1900 | "title": "The Hemingses of Monticello: An American Family", 1901 | "author": "Annette Gordon-Reed" 1902 | }, 1903 | { 1904 | "isbn": "0199285152", 1905 | "title": "The Legacy of Alexander: Politics, Warfare and Propaganda under the Successors", 1906 | "author": "A. B. Bosworth" 1907 | }, 1908 | { 1909 | "isbn": "1586833316", 1910 | "title": "Library Blogging", 1911 | "author": "Karen A. Coombs" 1912 | }, 1913 | { 1914 | "isbn": "9776163416", 1915 | "title": "Reflections on our digital future", 1916 | "author": "Ismail Serageldin" 1917 | }, 1918 | { 1919 | "isbn": "9776163483", 1920 | "title": "Women in Science: Time to Recognize the Obvious", 1921 | "author": "Ismail Serageldin" 1922 | }, 1923 | { 1924 | "isbn": "9776163998", 1925 | "title": "Much more than a building-- : reclaiming the legacy of the Bibliotheca Alexandrina", 1926 | "author": "Ismail Serageldin" 1927 | }, 1928 | { 1929 | "isbn": "1892391236", 1930 | "title": "Strange Itineraries", 1931 | "author": "Tim Powers" 1932 | }, 1933 | { 1934 | "isbn": "0385721706", 1935 | "title": "The Wisdom of Crowds", 1936 | "author": "James Surowiecki" 1937 | }, 1938 | { 1939 | "isbn": "0691024715", 1940 | "title": "Vladimir Nabokov : The American Years", 1941 | "author": "Brian Boyd" 1942 | }, 1943 | { 1944 | "isbn": "0691099006", 1945 | "title": "Mehmed the Conqueror and his time", 1946 | "author": "Franz Babinger" 1947 | }, 1948 | { 1949 | "isbn": "0060959355", 1950 | "title": "More Than You Know: A Novel", 1951 | "author": "Beth Gutcheon" 1952 | }, 1953 | { 1954 | "isbn": "0767420489", 1955 | "title": "How to Think About Weird Things: Critical Thinking for a New Age", 1956 | "author": "Theodore Schick" 1957 | }, 1958 | { 1959 | "isbn": "9780071549677", 1960 | "title": "How to do everything: Facebook applications", 1961 | "author": "Jesse Feiler" 1962 | }, 1963 | { 1964 | "isbn": "071561262X", 1965 | "title": "Julian the Apostate", 1966 | "author": "G. W. Bowersock" 1967 | }, 1968 | { 1969 | "isbn": "1400041988", 1970 | "title": "Pnin (Everyman's Library Classics & Contemporary Classics)", 1971 | "author": "Vladimir Nabokov" 1972 | }, 1973 | { 1974 | "isbn": "1883011191", 1975 | "title": "Nabokov: Novels 1955-1962: Lolita / Pnin / Pale Fire (Library of America)", 1976 | "author": "Vladimir Nabokov" 1977 | }, 1978 | { 1979 | "isbn": "0316015849", 1980 | "title": "Twilight (The Twilight Saga, Book 1)", 1981 | "author": "Stephenie Meyer" 1982 | }, 1983 | { 1984 | "isbn": "0756639808", 1985 | "title": "Real Sex for Real Women", 1986 | "author": "Laura Berman" 1987 | }, 1988 | { 1989 | "isbn": "0316043133", 1990 | "title": "Twilight: The Complete Illustrated Movie Companion", 1991 | "author": "" 1992 | }, 1993 | { 1994 | "isbn": "0307269639", 1995 | "title": "Nothing to Be Frightened Of", 1996 | "author": "Julian Barnes" 1997 | }, 1998 | { 1999 | "isbn": "0446579939", 2000 | "title": "The Lucky One", 2001 | "author": "Nicholas Sparks" 2002 | }, 2003 | { 2004 | "isbn": "1895523184", 2005 | "title": "The presidents' rap from Washington to Bush", 2006 | "author": "Sara Jordan" 2007 | }, 2008 | { 2009 | "isbn": "0545128285", 2010 | "title": "The Tales of Beedle the Bard, Standard Edition", 2011 | "author": "J. K. Rowling" 2012 | }, 2013 | { 2014 | "isbn": "0714848743", 2015 | "title": "Phaidon Atlas of 21st Century World Architecture", 2016 | "author": "Editors of Phaidon Press" 2017 | }, 2018 | { 2019 | "isbn": "8806193112", 2020 | "title": "Zona disagio", 2021 | "author": "Jonathan Franzen" 2022 | }, 2023 | { 2024 | "isbn": "8820046733", 2025 | "title": "La legione delle bambole. Un'inchiesta di Nathan Love", 2026 | "author": "Philip Le Roy" 2027 | }, 2028 | { 2029 | "isbn": "0385524382", 2030 | "title": "Sway: The Irresistible Pull of Irrational Behavior", 2031 | "author": "Ori Brafman" 2032 | }, 2033 | { 2034 | "isbn": "1573229156", 2035 | "title": "Three Apples Fell From Heaven", 2036 | "author": "Micheline Aharonian Marcom" 2037 | }, 2038 | { 2039 | "isbn": "0307101002", 2040 | "title": "Pooh Just Be Nice...to Your Little Friends! (Pooh - Just Be Nice Series)", 2041 | "author": "Caroline Kenneth" 2042 | }, 2043 | { 2044 | "isbn": "1400033888", 2045 | "title": "Istanbul: Memories and the City", 2046 | "author": "Orhan Pamuk" 2047 | }, 2048 | { 2049 | "isbn": "1931282528", 2050 | "title": "Incredible 5-Point Scale ¿ Assisting Students with Autism Spectrum Disorders in Understanding Social Interactions and Controlling Their Emotional Responses", 2051 | "author": "Kari Dunn Buron" 2052 | }, 2053 | { 2054 | "isbn": "0316043125", 2055 | "title": "The Twilight Saga: The Official Guide", 2056 | "author": "Stephenie Meyer" 2057 | }, 2058 | { 2059 | "isbn": "0061687200", 2060 | "title": "Marley & Me: Life and Love with the World's Worst Dog", 2061 | "author": "John Grogan" 2062 | }, 2063 | { 2064 | "isbn": "1894953479", 2065 | "title": "Installing Linux on a Dead Badger", 2066 | "author": "Lucy A. Snyder" 2067 | }, 2068 | { 2069 | "isbn": "0316031844", 2070 | "title": "The Twilight Saga: Slipcased", 2071 | "author": "Stephenie Meyer" 2072 | }, 2073 | { 2074 | "isbn": "0961392126", 2075 | "title": "Visual Explanations: Images and Quantities, Evidence and Narrative", 2076 | "author": "Edward R. Tufte" 2077 | }, 2078 | { 2079 | "isbn": "0961392142", 2080 | "title": "The Visual Display of Quantitative Information, 2nd edition", 2081 | "author": "Edward R. Tufte" 2082 | }, 2083 | { 2084 | "isbn": "0970661126", 2085 | "title": "Tickle His Pickle: Your Hands-On Guide to Penis Pleasing", 2086 | "author": "Sadie Allison" 2087 | }, 2088 | { 2089 | "isbn": "0520252667", 2090 | "title": "Mesopotamia: Assyrians, Sumerians, Babylonians", 2091 | "author": "Enrico Ascalone" 2092 | }, 2093 | { 2094 | "isbn": "0802713394", 2095 | "title": "Zarafa: a giraffe's true story, from deep in Africa to the heart of Paris", 2096 | "author": "Michael Allin" 2097 | }, 2098 | { 2099 | "isbn": "0844273341", 2100 | "title": "Practice Makes Perfect: Spanish Verb Tenses", 2101 | "author": "Dorothy Richmond" 2102 | }, 2103 | { 2104 | "isbn": "0199543305", 2105 | "title": "Little Oxford Dictionary of Quotations", 2106 | "author": "Susan Ratcliffe" 2107 | }, 2108 | { 2109 | "isbn": "0312263953", 2110 | "title": "Slightly Chipped: Footnotes in Booklore", 2111 | "author": "Lawrence Goldstone" 2112 | }, 2113 | { 2114 | "isbn": "1888553251", 2115 | "title": "Powder: Writing by Women in the Ranks, from Vietnam to Iraq", 2116 | "author": "Shannon Cain" 2117 | }, 2118 | { 2119 | "isbn": "1591842425", 2120 | "title": "Problem Solving 101: A Simple Book for Smart People", 2121 | "author": "Ken Watanabe" 2122 | }, 2123 | { 2124 | "isbn": "0143058754", 2125 | "title": "Mayflower: A Story of Courage, Community, and War", 2126 | "author": "Nathaniel Philbrick" 2127 | }, 2128 | { 2129 | "isbn": "0061686549", 2130 | "title": "What Have You Changed Your Mind About?: Today's Leading Minds Rethink Everything", 2131 | "author": "John Brockman" 2132 | }, 2133 | { 2134 | "isbn": "0978935403", 2135 | "title": "Not in a Thousand Years: The Uniqueness of the 20th Century", 2136 | "author": "Geoff Fernald" 2137 | }, 2138 | { 2139 | "isbn": "1847246869", 2140 | "title": "The landmark Herodotus: the histories", 2141 | "author": "Herodotus" 2142 | }, 2143 | { 2144 | "isbn": "019821913X", 2145 | "title": "The Legacy of Islam", 2146 | "author": "Joseph Schacht" 2147 | }, 2148 | { 2149 | "isbn": "0300120796", 2150 | "title": "Philip II of Macedonia", 2151 | "author": "Ian Worthington" 2152 | }, 2153 | { 2154 | "isbn": "0472110845", 2155 | "title": "Mountain and Plain: From the Lycian Coast to the Phrygian Plateau in the Late Roman and Early Byzantine Period", 2156 | "author": "Martin Harrison" 2157 | }, 2158 | { 2159 | "isbn": "1594743347", 2160 | "title": "Pride and Prejudice and Zombies: The Classic Regency Romance - Now with Ultraviolent Zombie Mayhem!", 2161 | "author": "Jane Austen" 2162 | }, 2163 | { 2164 | "isbn": "0307346609", 2165 | "title": "World War Z : an oral history of the zombie war", 2166 | "author": "Max Brooks" 2167 | }, 2168 | { 2169 | "isbn": "0156904365", 2170 | "title": "Till We Have Faces: A Myth Retold", 2171 | "author": "C.S. Lewis" 2172 | }, 2173 | { 2174 | "isbn": "0802141447", 2175 | "title": "Monster: The Autobiography of an L.A. Gang Member", 2176 | "author": "Sanyika Shakur" 2177 | }, 2178 | { 2179 | "isbn": "0345502833", 2180 | "title": "The Wednesday Sisters: A Novel", 2181 | "author": "Meg Waite Clayton" 2182 | }, 2183 | { 2184 | "isbn": "0747587035", 2185 | "title": "The ladies of Grace Adieu : and other stories", 2186 | "author": "Susanna Clarke" 2187 | }, 2188 | { 2189 | "isbn": "0981803989", 2190 | "title": "Just Fuck Me! - What Women Want Men to Know About Taking Control in the Bedroom (A Guide for Couples)", 2191 | "author": "Eve Kingsley" 2192 | }, 2193 | { 2194 | "isbn": "0316346624", 2195 | "title": "The Tipping Point: How Little Things Can Make a Big Difference", 2196 | "author": "Malcolm Gladwell" 2197 | }, 2198 | { 2199 | "isbn": "0451225856", 2200 | "title": "Lover Avenged (Black Dagger Brotherhood, Book 7)", 2201 | "author": "J.R. Ward" 2202 | }, 2203 | { 2204 | "isbn": "0786805838", 2205 | "title": "Whaley Whale (Thingy Things)", 2206 | "author": "Chris Raschka" 2207 | }, 2208 | { 2209 | "isbn": "9063531478", 2210 | "title": "Een huwelijk vol liefde", 2211 | "author": "Ed Wheat" 2212 | }, 2213 | { 2214 | "isbn": "0691006326", 2215 | "title": "The magician's doubts : Nabokov and the risks of fiction", 2216 | "author": "Michael Wood" 2217 | }, 2218 | { 2219 | "isbn": "0964380307", 2220 | "title": "Leif the lucky", 2221 | "author": "Ingri D'Aulaire" 2222 | }, 2223 | { 2224 | "isbn": "0807820202", 2225 | "title": "In the hands of Providence : Joshua L. Chamberlain and the American Civil War", 2226 | "author": "Alice Rains Trulock" 2227 | }, 2228 | { 2229 | "isbn": "0199552355", 2230 | "title": "The Last Man (Oxford World's Classics)", 2231 | "author": "Mary Wollstonecraft Shelley" 2232 | }, 2233 | { 2234 | "isbn": "0394588010", 2235 | "title": "A history of warfare", 2236 | "author": "John Keegan" 2237 | }, 2238 | { 2239 | "isbn": "0596517742", 2240 | "title": "JavaScript: The Good Parts", 2241 | "author": "Douglas Crockford" 2242 | }, 2243 | { 2244 | "isbn": "0520057376", 2245 | "title": "The Hellenistic World and the Coming of Rome", 2246 | "author": "Erich S. Gruen" 2247 | }, 2248 | { 2249 | "isbn": "1856079678", 2250 | "title": "Kenny's Choice: 101 Irish Books You Must Read", 2251 | "author": "Des Kenny" 2252 | }, 2253 | { 2254 | "isbn": "0486270556", 2255 | "title": "Six great Sherlock Holmes stories", 2256 | "author": "Sir Arthur Conan Doyle" 2257 | }, 2258 | { 2259 | "isbn": "0425030679", 2260 | "title": "Stranger In A Strange Land", 2261 | "author": "Robert A. Heinlein" 2262 | }, 2263 | { 2264 | "isbn": "9781934620069", 2265 | "title": "Make a zine! : when worlds and graphics collide", 2266 | "author": "Bill Brent" 2267 | }, 2268 | { 2269 | "isbn": "9780980200485", 2270 | "title": "So you want to be a librarian!", 2271 | "author": "Lauren Pressley" 2272 | }, 2273 | { 2274 | "isbn": "0981794106", 2275 | "title": "Stolen Sharpie Revolution 2: a DIY resource to zines and zine culture", 2276 | "author": "Alex Wrekk" 2277 | }, 2278 | { 2279 | "isbn": "0061142026", 2280 | "title": "Stardust", 2281 | "author": "Neil Gaiman" 2282 | }, 2283 | { 2284 | "isbn": "0299118045", 2285 | "title": "Wit and the writing of history : the rhetoric of historiography in imperial Rome", 2286 | "author": "Paul Plass" 2287 | }, 2288 | { 2289 | "isbn": "9781591842231", 2290 | "title": "Reality check : the irreverent guide to outsmarting, outmanaging, and outmarketing your competition", 2291 | "author": "Guy Kawasaki" 2292 | }, 2293 | { 2294 | "isbn": "0231148143", 2295 | "title": "The Late Age of Print: Everyday Book Culture from Consumerism to Control", 2296 | "author": "Ted Striphas" 2297 | }, 2298 | { 2299 | "isbn": "0140148221", 2300 | "title": "An imperial possession : Britain in the Roman Empire, 54 BC-AD 409", 2301 | "author": "D. J. Mattingly" 2302 | }, 2303 | { 2304 | "isbn": "0143114948", 2305 | "title": "Here Comes Everybody: The Power of Organizing Without Organizations", 2306 | "author": "Clay Shirky" 2307 | }, 2308 | { 2309 | "isbn": "0316826200", 2310 | "title": "Of Beetles and Angels: A Boy's Remarkable Journey from a Refugee Camp to Harvard", 2311 | "author": "Mawi Asgedom" 2312 | }, 2313 | { 2314 | "isbn": "0307346617", 2315 | "title": "World War Z: An Oral History of the Zombie War", 2316 | "author": "Max Brooks" 2317 | }, 2318 | { 2319 | "isbn": "0446509337", 2320 | "title": "Soul Survivor: The Reincarnation of a World War II Fighter Pilot", 2321 | "author": "Andrea Leininger" 2322 | }, 2323 | { 2324 | "isbn": "0785126708", 2325 | "title": "World War Hulk", 2326 | "author": "Greg Pak" 2327 | }, 2328 | { 2329 | "isbn": "0471488550", 2330 | "title": "Robust Regression and Outlier Detection (Wiley Series in Probability and Statistics)", 2331 | "author": "Peter J. Rousseeuw" 2332 | }, 2333 | { 2334 | "isbn": "0595132820", 2335 | "title": "The Outlier", 2336 | "author": "RJ Stanton" 2337 | }, 2338 | { 2339 | "isbn": "075663007X", 2340 | "title": "World War I (DK Eyewitness Books)", 2341 | "author": "Simon Adams" 2342 | }, 2343 | { 2344 | "isbn": "1574889990", 2345 | "title": "Untold Valor: Forgotten Stories of American Bomber Crews over Europe in World War II", 2346 | "author": "Rob Morris" 2347 | }, 2348 | { 2349 | "isbn": "0915572168", 2350 | "title": "Here comes everybody : new & selected poems", 2351 | "author": "Madeline Gleason" 2352 | }, 2353 | { 2354 | "isbn": "1400034728", 2355 | "title": "When Bad Things Happen to Good People", 2356 | "author": "Harold S. Kushner" 2357 | }, 2358 | { 2359 | "isbn": "0767902890", 2360 | "title": "The Things They Carried", 2361 | "author": "Tim O'Brien" 2362 | }, 2363 | { 2364 | "isbn": "0972545425", 2365 | "title": "Sparkling Gems From The Greek: 365 Greek Word Studies For Every Day Of The Year To Sharpen Your Understanding Of God's Word", 2366 | "author": "Rick Renner" 2367 | }, 2368 | { 2369 | "isbn": "1557047952", 2370 | "title": "Michael Clayton: The Shooting Script (Newmarket Shooting Scripts)", 2371 | "author": "Tony Gilroy" 2372 | }, 2373 | { 2374 | "isbn": "1414325460", 2375 | "title": "The Secret Holocaust Diaries: The Untold Story of Nonna Bannister", 2376 | "author": "Nonna Bannister" 2377 | }, 2378 | { 2379 | "isbn": "0323053459", 2380 | "title": "Mosby's Diagnostic and Laboratory Test Reference", 2381 | "author": "Kathleen Deska Pagana PhD RN" 2382 | }, 2383 | { 2384 | "isbn": "1846590671", 2385 | "title": "Many and Many A Year Ago", 2386 | "author": "Selcuk Altun" 2387 | }, 2388 | { 2389 | "isbn": "0078271282", 2390 | "title": "Mathematics for Grob Basic Electronics", 2391 | "author": "Bernard Grob" 2392 | }, 2393 | { 2394 | "isbn": "0821258109", 2395 | "title": "Picasso & Lump: A Dachshund's Odyssey", 2396 | "author": "David Douglas Duncan" 2397 | }, 2398 | { 2399 | "isbn": "0061574287", 2400 | "title": "The Lump of Coal", 2401 | "author": "Lemony Snicket" 2402 | }, 2403 | { 2404 | "isbn": "0531108651", 2405 | "title": "Lumps, Bumps, and Rashes (A First Book)", 2406 | "author": "Alan Edward Nourse" 2407 | }, 2408 | { 2409 | "isbn": "0789737051", 2410 | "title": "NCLEX-RN Exam Cram (2nd Edition)", 2411 | "author": "Wilda Rinehart" 2412 | }, 2413 | { 2414 | "isbn": "0805443908", 2415 | "title": "Simple Church: Returning to God's Process for Making Disciples", 2416 | "author": "Thom S. Rainer" 2417 | }, 2418 | { 2419 | "isbn": "1416566112", 2420 | "title": "Ratio: The Simple Codes Behind the Craft of Everyday Cooking", 2421 | "author": "Michael Ruhlman" 2422 | }, 2423 | { 2424 | "isbn": "0979106613", 2425 | "title": "No-Nonsense Craps: The Consummate Guide to Winning at the Crap Table", 2426 | "author": "Richard Orlyn" 2427 | }, 2428 | { 2429 | "isbn": "1581809662", 2430 | "title": "Mr. Funky's Super Crochet Wonderful", 2431 | "author": "Narumi Ogawa" 2432 | }, 2433 | { 2434 | "isbn": "0974345407", 2435 | "title": "Xero: Turn-of-the-Millenia (Zero)", 2436 | "author": "La Ruocco" 2437 | }, 2438 | { 2439 | "isbn": "2890216152", 2440 | "title": "Le Temple De Xeros (Roman Jeunesse, 125) (Spanish Edition)", 2441 | "author": "Raymond Plante" 2442 | }, 2443 | { 2444 | "isbn": "1402742126", 2445 | "title": "Living with Books", 2446 | "author": "Alan Powers" 2447 | }, 2448 | { 2449 | "isbn": "383279204X", 2450 | "title": "Library Design", 2451 | "author": "teNeues" 2452 | }, 2453 | { 2454 | "isbn": "0385607024", 2455 | "title": "The Prester Quest", 2456 | "author": "Nicholas Jubber" 2457 | }, 2458 | { 2459 | "isbn": "050028816X", 2460 | "title": "Lost Languages: The Enigma of the World's Undeciphered Scripts", 2461 | "author": "Andrew Robinson" 2462 | }, 2463 | { 2464 | "isbn": "0786435437", 2465 | "title": "Radical Cataloging: Essays at the Front", 2466 | "author": "K. R. Roberto" 2467 | }, 2468 | { 2469 | "isbn": "0385504225", 2470 | "title": "The Lost Symbol", 2471 | "author": "Dan Brown" 2472 | }, 2473 | { 2474 | "isbn": "0313323569", 2475 | "title": "The History of New Zealand (The Greenwood Histories of the Modern Nations)", 2476 | "author": "Tom Brooking" 2477 | }, 2478 | { 2479 | "isbn": "0143018671", 2480 | "title": "The Penguin History of New Zealand", 2481 | "author": "Michael King" 2482 | }, 2483 | { 2484 | "isbn": "1594132984", 2485 | "title": "A Voyage Long and Strange: Rediscovering the New World", 2486 | "author": "Tony Horwitz" 2487 | }, 2488 | { 2489 | "isbn": "0061177571", 2490 | "title": "post office: A Novel", 2491 | "author": "Charles Bukowski" 2492 | }, 2493 | { 2494 | "isbn": "0141189827", 2495 | "title": "Junky: The Definitive Text of 'Junk' (Penguin Modern Classics)", 2496 | "author": "William S. Burroughs" 2497 | }, 2498 | { 2499 | "isbn": "0898706408", 2500 | "title": "Salt of the Earth: The Church at the End of the Millennium: An Interview With Peter Seewald", 2501 | "author": "Joseph Cardinal Ratzinger" 2502 | }, 2503 | { 2504 | "isbn": "0618057021", 2505 | "title": "J.R.R. Tolkien: A Biography", 2506 | "author": "Humphrey Carpenter" 2507 | }, 2508 | { 2509 | "isbn": "0823210502", 2510 | "title": "The secret of world history : selected writings on the art and science of history", 2511 | "author": "Leopold von Ranke" 2512 | }, 2513 | { 2514 | "isbn": "3446412190", 2515 | "title": "Wikinomics", 2516 | "author": "Anthony D. Williams" 2517 | }, 2518 | { 2519 | "isbn": "0691126836", 2520 | "title": "The Poison King: The Life and Legend of Mithradates, Rome's Deadliest Enemy", 2521 | "author": "Adrienne Mayor" 2522 | }, 2523 | { 2524 | "isbn": "0875010601", 2525 | "title": "Phantom of fact : a guide to Nabokov's Pnin", 2526 | "author": "Gennady Barabtarlo" 2527 | }, 2528 | { 2529 | "isbn": "0307269647", 2530 | "title": "You Are Not a Gadget: A Manifesto", 2531 | "author": "Jaron Lanier" 2532 | }, 2533 | { 2534 | "isbn": "0061472808", 2535 | "title": "The lost history of Christianity : the thousand-year golden age of the church in the Middle East, Africa, and Asia- and how it died", 2536 | "author": "Philip Jenkins" 2537 | }, 2538 | { 2539 | "isbn": "0312254199", 2540 | "title": "Signposts in a Strange Land: Essays", 2541 | "author": "Walker Percy" 2542 | }, 2543 | { 2544 | "isbn": "0977872610", 2545 | "title": "Love Yourself and Let the Other Person Have It Your Way", 2546 | "author": "Lawrence Crane" 2547 | }, 2548 | { 2549 | "isbn": "0415299098", 2550 | "title": "The History of Zonaras: From Alexander Severus to the Death of Theodosius the Great (Routledge Classical Translations)", 2551 | "author": "Thomas Banchich" 2552 | }, 2553 | { 2554 | "isbn": "0670069280", 2555 | "title": "The War Memoirs Of Hrh Wallis Duchess Of Windsor", 2556 | "author": "Kate Auspitz" 2557 | }, 2558 | { 2559 | "isbn": "0521095174", 2560 | "title": "Interpretation of the fourth Gospel", 2561 | "author": "C. H. Dodd" 2562 | }, 2563 | { 2564 | "isbn": "0142402575", 2565 | "title": "The House With a Clock In Its Walls (Lewis Barnavelt)", 2566 | "author": "John Bellairs" 2567 | }, 2568 | { 2569 | "isbn": "9781596299368", 2570 | "title": "Strange Maine : true tales from the Pine Tree State", 2571 | "author": "Michelle Souliere" 2572 | }, 2573 | { 2574 | "isbn": "0674005066", 2575 | "title": "History of the Florentine people", 2576 | "author": "Leonardo Bruni" 2577 | }, 2578 | { 2579 | "isbn": "9788844033590", 2580 | "title": "Tuscan cuisine: book of recipes", 2581 | "author": "Guido Pedrittoni" 2582 | }, 2583 | { 2584 | "isbn": "1894031911", 2585 | "title": "The Logogryph: A Bibliography Of Imaginary Books", 2586 | "author": "Thomas Wharton" 2587 | }, 2588 | { 2589 | "isbn": "0300115962", 2590 | "title": "Voting About God in Early Church Councils", 2591 | "author": "Professor Ramsay MacMullen" 2592 | }, 2593 | { 2594 | "isbn": "0195153855", 2595 | "title": "The Mandaeans: Ancient Texts and Modern People (Aar the Religions (Unnumbered).)", 2596 | "author": "Jorunn Jacobsen Buckley" 2597 | }, 2598 | { 2599 | "isbn": "0140442340", 2600 | "title": "Life of Appollonius", 2601 | "author": "Philostratus.," 2602 | }, 2603 | { 2604 | "isbn": "1889119040", 2605 | "title": "The Quest: The Search for the Historical Jesus and Muhammad", 2606 | "author": "F.E. Peters" 2607 | }, 2608 | { 2609 | "isbn": "0786711922", 2610 | "title": "The real Eve : modern man's journey out of Africa", 2611 | "author": "Stephen Oppenheimer" 2612 | }, 2613 | { 2614 | "isbn": "1565859480", 2615 | "title": "The story of human language", 2616 | "author": "John H McWhorter" 2617 | }, 2618 | { 2619 | "isbn": "1904233805", 2620 | "title": "Twilight", 2621 | "author": "Stephenie Meyer" 2622 | }, 2623 | { 2624 | "isbn": "0764291009", 2625 | "title": "Love's Unending Legacy/Love's Unfolding Dream/Love Takes Wing/Love Finds a Home (Love Comes Softly Series 5-8)", 2626 | "author": "Janette Oke" 2627 | }, 2628 | { 2629 | "isbn": "0195182499", 2630 | "title": "Lost Christianities: The Battles for Scripture and the Faiths We Never Knew", 2631 | "author": "Bart D. Ehrman" 2632 | }, 2633 | { 2634 | "isbn": "0312605390", 2635 | "title": "Love in the Afternoon (Hathaways, Book 5)", 2636 | "author": "Lisa Kleypas" 2637 | }, 2638 | { 2639 | "isbn": "0802841805", 2640 | "title": "The Scandal of the Evangelical Mind", 2641 | "author": "Mark A. Noll" 2642 | }, 2643 | { 2644 | "isbn": "0553446398", 2645 | "title": "Hidden treasure", 2646 | "author": "Cheryln Biggs" 2647 | }, 2648 | { 2649 | "isbn": "0520075641", 2650 | "title": "Hellenistic history and culture", 2651 | "author": "Peter Green" 2652 | }, 2653 | { 2654 | "isbn": "1400066409", 2655 | "title": "Super Sad True Love Story: A Novel", 2656 | "author": "Gary Shteyngart" 2657 | }, 2658 | { 2659 | "isbn": "0865540489", 2660 | "title": "Introduction to Sahidic Coptic", 2661 | "author": "Thomas Oden Lambdin" 2662 | }, 2663 | { 2664 | "isbn": "0786166800", 2665 | "title": "What Paul Meant", 2666 | "author": "Garry Wills" 2667 | }, 2668 | { 2669 | "isbn": "0670034967", 2670 | "title": "What Jesus Meant", 2671 | "author": "Garry Wills" 2672 | }, 2673 | { 2674 | "isbn": "014311512X", 2675 | "title": "What the Gospels Meant", 2676 | "author": "Garry Wills" 2677 | }, 2678 | { 2679 | "isbn": "141654335X", 2680 | "title": "Under God: Religion and American Politics", 2681 | "author": "Garry Wills" 2682 | }, 2683 | { 2684 | "isbn": "9781415961759", 2685 | "title": "Passage", 2686 | "author": "" 2687 | }, 2688 | { 2689 | "isbn": "9780307738325", 2690 | "title": "Turtle in paradise", 2691 | "author": "Jennifer L. Holm" 2692 | }, 2693 | { 2694 | "isbn": "9780307710949", 2695 | "title": "The water seeker", 2696 | "author": "Kimberly Willis Holt" 2697 | }, 2698 | { 2699 | "isbn": "9780307737236", 2700 | "title": "The vigilantes", 2701 | "author": "W. E. B. Griffin" 2702 | }, 2703 | { 2704 | "isbn": "9780307715630", 2705 | "title": "This body of death", 2706 | "author": "Elizabeth George" 2707 | }, 2708 | { 2709 | "isbn": "9780307735270", 2710 | "title": "My name is memory : [a novel]", 2711 | "author": "Ann Brashares" 2712 | }, 2713 | { 2714 | "isbn": "9780307736055", 2715 | "title": "Lucid intervals", 2716 | "author": "Stuart Woods" 2717 | }, 2718 | { 2719 | "isbn": "9780307749260", 2720 | "title": "Oprah : [a biography]", 2721 | "author": "Kitty Kelley" 2722 | }, 2723 | { 2724 | "isbn": "0802714986", 2725 | "title": "Sea of faith : Islam and Christianity in the medieval Mediterranean world", 2726 | "author": "Stephen O'Shea" 2727 | }, 2728 | { 2729 | "isbn": "014144178X", 2730 | "title": "The Talmud: A Selection (Penguin Classics)", 2731 | "author": "Norman Solomon" 2732 | }, 2733 | { 2734 | "isbn": "0199291535", 2735 | "title": "Seeing the Face, Seeing the Soul: Polemon's Physiognomy from Classical Antiquity to Medieval Islam", 2736 | "author": "George Boys-Stones" 2737 | }, 2738 | { 2739 | "isbn": "9783438054012", 2740 | "title": "Novum testamentum Graece et Latine", 2741 | "author": "Eberhard Nestle" 2742 | }, 2743 | { 2744 | "isbn": "0374272387", 2745 | "title": "The Talmud and the Internet : a journey between worlds", 2746 | "author": "Jonathan Rosen" 2747 | }, 2748 | { 2749 | "isbn": "0664220754", 2750 | "title": "The Israelites in history and tradition", 2751 | "author": "Niels Peter Lemche" 2752 | }, 2753 | { 2754 | "isbn": "0596806027", 2755 | "title": "HTML5: Up and Running", 2756 | "author": "Mark Pilgrim" 2757 | }, 2758 | { 2759 | "isbn": "1562762400", 2760 | "title": "The Internet by E-mail", 2761 | "author": "Clay Shirky" 2762 | }, 2763 | { 2764 | "isbn": "0380815931", 2765 | "title": "In the beginning ...was the command line", 2766 | "author": "Neal Stephenson" 2767 | }, 2768 | { 2769 | "isbn": "0520058763", 2770 | "title": "The making of Citizen Kane", 2771 | "author": "Robert L. Carringer" 2772 | }, 2773 | { 2774 | "isbn": "9780316017923", 2775 | "title": "Outliers : the story of success", 2776 | "author": "Malcolm Gladwell" 2777 | }, 2778 | { 2779 | "isbn": "0066620694", 2780 | "title": "The innovator's dilemma : when new technologies cause great firms to fail", 2781 | "author": "Clayton M. Christensen" 2782 | }, 2783 | { 2784 | "isbn": "0394748808", 2785 | "title": "Oracles and Divination", 2786 | "author": "Michael Loewe" 2787 | }, 2788 | { 2789 | "isbn": "0585350701", 2790 | "title": "A religious history of the American people", 2791 | "author": "Sydney E. Ahlstrom" 2792 | }, 2793 | { 2794 | "isbn": "0399536493", 2795 | "title": "Images You Should Not Masturbate To", 2796 | "author": "Graham Johnson" 2797 | }, 2798 | { 2799 | "isbn": "0801862655", 2800 | "title": "The Smoke of Satan: Conservative and Traditionalist Dissent in Contemporary American Catholicism", 2801 | "author": "Michael W. Cuneo" 2802 | }, 2803 | { 2804 | "isbn": "0821413325", 2805 | "title": "Lord Of Visible World: Autobiography In Letters", 2806 | "author": "H.P. Lovecraft" 2807 | }, 2808 | { 2809 | "isbn": "0451037529", 2810 | "title": "The Puppet Masters (Signet SF, T3752)", 2811 | "author": "Robert A. Heinlein" 2812 | }, 2813 | { 2814 | "isbn": "0465023975", 2815 | "title": "Osman's Dream: The History of the Ottoman Empire", 2816 | "author": "Caroline Finkel" 2817 | }, 2818 | { 2819 | "isbn": "0786891076", 2820 | "title": "Shopgirl: A Novella", 2821 | "author": "Steve Martin" 2822 | }, 2823 | { 2824 | "isbn": "0446557021", 2825 | "title": "Late for School", 2826 | "author": "Steve Martin" 2827 | }, 2828 | { 2829 | "isbn": "1601420617", 2830 | "title": "Redeeming Love", 2831 | "author": "Francine Rivers" 2832 | }, 2833 | { 2834 | "isbn": "0674049683", 2835 | "title": "Reading and Writing in Babylon", 2836 | "author": "Dominique Charpin" 2837 | }, 2838 | { 2839 | "isbn": "1404861084", 2840 | "title": "My Friend Has ADHD (Friends With Disabilities)", 2841 | "author": "Amanda Doering Tourville" 2842 | }, 2843 | { 2844 | "isbn": "1404861106", 2845 | "title": "My Friend Has Down Syndrome (Friends With Disabilities)", 2846 | "author": "Amanda Doering Tourville" 2847 | }, 2848 | { 2849 | "isbn": "160942168X", 2850 | "title": "War and Peace", 2851 | "author": "Leo Tolstoy" 2852 | }, 2853 | { 2854 | "isbn": "1400079985", 2855 | "title": "War and Peace (Vintage Classics)", 2856 | "author": "Leo Tolstoy" 2857 | }, 2858 | { 2859 | "isbn": "158731455X", 2860 | "title": "The Latin Letters of C.S. Lewis", 2861 | "author": "C.S. Lewis" 2862 | }, 2863 | { 2864 | "isbn": "0395938473", 2865 | "title": "The New Way Things Work", 2866 | "author": "David Macaulay" 2867 | }, 2868 | { 2869 | "isbn": "0812550757", 2870 | "title": "Speaker for the Dead (Ender, Book 2)", 2871 | "author": "Orson Scott Card" 2872 | }, 2873 | { 2874 | "isbn": "9780061947032", 2875 | "title": "Revelation of the Magi : the lost tale of the Three Wise Men's journey to Bethlehem", 2876 | "author": "Brent Landau" 2877 | }, 2878 | { 2879 | "isbn": "9780470568439", 2880 | "title": "iPhone application development for dummies", 2881 | "author": "Neal L. Goldstein" 2882 | }, 2883 | { 2884 | "isbn": "9781586176068", 2885 | "title": "Light of the world : the pope, the church, and the signs of the times: a conversation with peter seewald", 2886 | "author": "Peter Seewald" 2887 | }, 2888 | { 2889 | "isbn": "0801879523", 2890 | "title": "Tasmanian tiger : the tragic tale of how the world lost its most mysterious predator", 2891 | "author": "David Owen" 2892 | }, 2893 | { 2894 | "isbn": "0006273297", 2895 | "title": "Letters of C.S. Lewis", 2896 | "author": "C. S. Lewis" 2897 | }, 2898 | { 2899 | "isbn": "9780674047495", 2900 | "title": "What Happened at Vatican II", 2901 | "author": "John W. O'Malley S. J." 2902 | }, 2903 | { 2904 | "isbn": "0814317464", 2905 | "title": "Ovid's games of love", 2906 | "author": "Molly Myerowitz" 2907 | }, 2908 | { 2909 | "isbn": "030726839X", 2910 | "title": "Winston's War: Churchill, 1940-1945", 2911 | "author": "Max Hastings" 2912 | }, 2913 | { 2914 | "isbn": "159629955X", 2915 | "title": "Portlands Greatest Conflagration: The 1866 Fire Disaster (ME)", 2916 | "author": "Michael Daicy" 2917 | }, 2918 | { 2919 | "isbn": "0385533853", 2920 | "title": "Robopocalypse: A Novel", 2921 | "author": "Daniel H. Wilson" 2922 | }, 2923 | { 2924 | "isbn": "0380794802", 2925 | "title": "One True Love", 2926 | "author": "Barbara Freethy" 2927 | }, 2928 | { 2929 | "isbn": "0800601270", 2930 | "title": "The Worship of the Early Church", 2931 | "author": "Ferdinand Hahn" 2932 | }, 2933 | { 2934 | "isbn": "080520413X", 2935 | "title": "The Jewish Festivals: History and Observance (English and Hebrew Edition)", 2936 | "author": "Hayyim Schauss" 2937 | }, 2938 | { 2939 | "isbn": "039333869X", 2940 | "title": "Liar's Poker", 2941 | "author": "Michael Lewis" 2942 | }, 2943 | { 2944 | "isbn": "1571780793", 2945 | "title": "Twilight of the clockwork God : conversations on science and spirituality at the end of an age", 2946 | "author": "John David Ebert" 2947 | }, 2948 | { 2949 | "isbn": "9788799197965", 2950 | "title": "Linda Love", 2951 | "author": "Asbjørn Auring Grimm" 2952 | }, 2953 | { 2954 | "isbn": "1565970551", 2955 | "title": "Love in bloom (Kismet)", 2956 | "author": "Karen Rose Smith" 2957 | }, 2958 | { 2959 | "isbn": "0805422994", 2960 | "title": "We Remember C. S. Lewis: Essays and Memoirs by Philip Yancey, J. I.Packer, Charles Colson, George Sayer, James Houston, Don Bede Griffiths and Others", 2961 | "author": "David Graham" 2962 | }, 2963 | { 2964 | "isbn": "0199573506", 2965 | "title": "Planets: A Very Short Introduction (Very Short Introductions)", 2966 | "author": "David A. Rothery" 2967 | }, 2968 | { 2969 | "isbn": "1592406254", 2970 | "title": "What Language Is: And What It Isn’t and What It Could Be", 2971 | "author": "John McWhorter" 2972 | }, 2973 | { 2974 | "isbn": "1861978375", 2975 | "title": "We-Think: Mass innovation, not mass production", 2976 | "author": "Charles Leadbeater" 2977 | }, 2978 | { 2979 | "isbn": "0446601977", 2980 | "title": "Parable of the Sower", 2981 | "author": "Octavia E. Butler" 2982 | }, 2983 | { 2984 | "isbn": "0380006650", 2985 | "title": "The Investigation", 2986 | "author": "Stanislaw Lem" 2987 | }, 2988 | { 2989 | "isbn": "0143016695", 2990 | "title": "Ysabel", 2991 | "author": "Guy Gavriel Kay" 2992 | }, 2993 | { 2994 | "isbn": "0060692545", 2995 | "title": "The Triumph of the Meek: Why Early Christianity Succeeded", 2996 | "author": "Michael J. Walsh" 2997 | }, 2998 | { 2999 | "isbn": "9780765316738", 3000 | "title": "The trade of queens", 3001 | "author": "Charles Stross" 3002 | }, 3003 | { 3004 | "isbn": "8838436355", 3005 | "title": "Il mistero dell'Isola del Drago", 3006 | "author": "Renato Giovannoli" 3007 | }, 3008 | { 3009 | "isbn": "0877854106", 3010 | "title": "A Swedenborg Sampler: Selections from Heaven and Hell, Divine Love and Wisdom, Divine Providence, True Christianity, and Secrets of Heaven", 3011 | "author": "Emanuel Swedenborg" 3012 | }, 3013 | { 3014 | "isbn": "0300176880", 3015 | "title": "Ten Popes Who Shook the World", 3016 | "author": "Eamon Duffy" 3017 | }, 3018 | { 3019 | "isbn": "014311302X", 3020 | "title": "Pope John XXIII: A Life (Penguin Lives)", 3021 | "author": "Thomas Cahill" 3022 | }, 3023 | { 3024 | "isbn": "0486424758", 3025 | "title": "Simplified grammar of Arabic, Persian, and Hindustani", 3026 | "author": "Edward Henry Palmer" 3027 | }, 3028 | { 3029 | "isbn": "0394438957", 3030 | "title": "Of Plymouth Plantation, 1620-1647", 3031 | "author": "William Bradford" 3032 | }, 3033 | { 3034 | "isbn": "0195297709", 3035 | "title": "The Jewish Annotated New Testament", 3036 | "author": "Amy-Jill Levine" 3037 | }, 3038 | { 3039 | "isbn": "0810993465", 3040 | "title": "Library Mouse", 3041 | "author": "Daniel Kirk" 3042 | }, 3043 | { 3044 | "isbn": "076363784X", 3045 | "title": "Library Lion", 3046 | "author": "Michelle Knudsen" 3047 | }, 3048 | { 3049 | "isbn": "0618968636", 3050 | "title": "The Hobbit", 3051 | "author": "J.R.R. Tolkien" 3052 | }, 3053 | { 3054 | "isbn": "0802839312", 3055 | "title": "Jesus Remembered (Christianity in the Making)", 3056 | "author": "James D. G. Dunn" 3057 | }, 3058 | { 3059 | "isbn": "1567505198", 3060 | "title": "Sorting Out the Web: Approaches to Subject Access (Contemporary Studies in Information Management, Policy, and)", 3061 | "author": "Candy Schwartz" 3062 | }, 3063 | { 3064 | "isbn": "1936383632", 3065 | "title": "Jimmy Plush, Teddy Bear Detective", 3066 | "author": "Garrett Cook" 3067 | }, 3068 | { 3069 | "isbn": "1582406197", 3070 | "title": "The Walking Dead, Book 1 (Bk. 1)", 3071 | "author": "Robert Kirkman" 3072 | }, 3073 | { 3074 | "isbn": "9038826842", 3075 | "title": "Serieuze poging tot een volledige bibliografie van de zelfstandige en verspreide geschriften van Arnon Grunberg waarin opgenomen diens Interview met mijn bibliograaf & In ieder mens schuilt een maniak", 3076 | "author": "Wuijts" 3077 | }, 3078 | { 3079 | "isbn": "1451648537", 3080 | "title": "Steve Jobs", 3081 | "author": "Walter Isaacson" 3082 | }, 3083 | { 3084 | "isbn": "0375706399", 3085 | "title": "Test Book", 3086 | "author": "Test Book" 3087 | }, 3088 | { 3089 | "isbn": "0061431605", 3090 | "title": "This Book Is Overdue!: How Librarians and Cybrarians Can Save Us All", 3091 | "author": "Marilyn Johnson" 3092 | }, 3093 | { 3094 | "isbn": "059529362X", 3095 | "title": "HIP Recollections of Darby Hicks: (The Musings of a Senior Bronx Homeboy)", 3096 | "author": "Bob Washington" 3097 | }, 3098 | { 3099 | "isbn": "1592769446", 3100 | "title": "Holiness for Everyone: The Practical Spirituality of St. Josemaria Escriva", 3101 | "author": "Eric Sammons" 3102 | }, 3103 | { 3104 | "isbn": "9781892391896", 3105 | "title": "Test book", 3106 | "author": "Test Book" 3107 | }, 3108 | { 3109 | "isbn": "1580512283", 3110 | "title": "A History of the Popes: From Peter to the Present", 3111 | "author": "John O'Malley" 3112 | }, 3113 | { 3114 | "isbn": "0674066979", 3115 | "title": "Trent: What Happened at the Council", 3116 | "author": "John W. O'Malley" 3117 | }, 3118 | { 3119 | "isbn": "0800625242", 3120 | "title": "Introduction to the Talmud and Midrash", 3121 | "author": "Hermann L. Strack" 3122 | }, 3123 | { 3124 | "isbn": "0099286246", 3125 | "title": "The lawless roads", 3126 | "author": "Graham Greene" 3127 | }, 3128 | { 3129 | "isbn": "0814680291", 3130 | "title": "My Journal of the Council", 3131 | "author": "Yves Congar" 3132 | }, 3133 | { 3134 | "isbn": "0140010750", 3135 | "title": "Kraken Wakes", 3136 | "author": "John Wyndham" 3137 | }, 3138 | { 3139 | "isbn": "0575047674", 3140 | "title": "Wulfsyarn", 3141 | "author": "Phillip Mann" 3142 | }, 3143 | { 3144 | "isbn": "0898707021", 3145 | "title": "Milestones : memoirs, 1927-1977", 3146 | "author": "Pope Benedict" 3147 | }, 3148 | { 3149 | "isbn": "9780441015146", 3150 | "title": "Plague year", 3151 | "author": "Jeff Carlson" 3152 | }, 3153 | { 3154 | "isbn": "0821768050", 3155 | "title": "Lady May's folly", 3156 | "author": "Donna Simpson" 3157 | }, 3158 | { 3159 | "isbn": "0664236995", 3160 | "title": "Tokens of Trust: An Introduction to Christian Belief", 3161 | "author": "Rowan Williams" 3162 | }, 3163 | { 3164 | "isbn": "0385497938", 3165 | "title": "Rabbi Jesus: An Intimate Biography", 3166 | "author": "Bruce Chilton" 3167 | }, 3168 | { 3169 | "isbn": "1619697688", 3170 | "title": "The Onion Book of Known Knowledge", 3171 | "author": "The Onion" 3172 | }, 3173 | { 3174 | "isbn": "0780786068", 3175 | "title": "Merlin and the Dragons", 3176 | "author": "Jane Yolen" 3177 | }, 3178 | { 3179 | "isbn": "0567292061", 3180 | "title": "Rome and the Eastern Churches", 3181 | "author": "Aidan Nichols" 3182 | }, 3183 | { 3184 | "isbn": "0199754381", 3185 | "title": "Leaves from the Garden of Eden", 3186 | "author": "Howard Schwartz" 3187 | }, 3188 | { 3189 | "isbn": "1461057205", 3190 | "title": "Wool", 3191 | "author": "Hugh Howey" 3192 | }, 3193 | { 3194 | "isbn": "019504391X", 3195 | "title": "Hellenistic Religions: An Introduction", 3196 | "author": "Luther H. Martin" 3197 | }, 3198 | { 3199 | "isbn": "0300056400", 3200 | "title": "The origins of Christian morality : the first two centuries", 3201 | "author": "Wayne A. Meeks" 3202 | }, 3203 | { 3204 | "isbn": "0060182148", 3205 | "title": "Mariette in Ecstasy", 3206 | "author": "Ron Hansen" 3207 | }, 3208 | { 3209 | "isbn": "0691081948", 3210 | "title": "Why big fierce animals are rare : an ecologist's perspective", 3211 | "author": "Paul A. Colinvaux" 3212 | }, 3213 | { 3214 | "isbn": "0674996364", 3215 | "title": "Hellenistic Collection: Philitas. Alexander of Aetolia. Hermesianax. Euphorion. Parthenius (Loeb Classical Library)", 3216 | "author": "J. L. Lightfoot" 3217 | }, 3218 | { 3219 | "isbn": "0674073142", 3220 | "title": "Homer's Turk: How Classics Shaped Ideas of the East", 3221 | "author": "Jerry Toner" 3222 | }, 3223 | { 3224 | "isbn": "080104538X", 3225 | "title": "Jesus the Temple", 3226 | "author": "Nicholas Perrin" 3227 | }, 3228 | { 3229 | "isbn": "0801488729", 3230 | "title": "The Origin of Sin: An English Translation of the Hamartigenia (Cornell Studies in Classical Philology)", 3231 | "author": "Prudentius" 3232 | }, 3233 | { 3234 | "isbn": "1844130533", 3235 | "title": "How to Read a Church", 3236 | "author": "Richard Taylor" 3237 | }, 3238 | { 3239 | "isbn": "2266182714", 3240 | "title": "Hunger Games 3/LA Revolte (French Edition)", 3241 | "author": "Suzanne Collins" 3242 | }, 3243 | { 3244 | "isbn": "143915354X", 3245 | "title": "The Judas Gospel: A Novel", 3246 | "author": "Bill Myers" 3247 | }, 3248 | { 3249 | "isbn": "1570759936", 3250 | "title": "Vatican II: Fifty Personal Stories", 3251 | "author": "William Madges" 3252 | }, 3253 | { 3254 | "isbn": "1856077896", 3255 | "title": "Reaping the Harvest: Fifty Years After Vatican II", 3256 | "author": "Suzanne Mulligan" 3257 | }, 3258 | { 3259 | "isbn": "0670024872", 3260 | "title": "Why Priests?: A Failed Tradition", 3261 | "author": "Garry Wills" 3262 | }, 3263 | { 3264 | "isbn": "037542346X", 3265 | "title": "The Landmark Arrian: The Campaigns of Alexander", 3266 | "author": "James Romm" 3267 | }, 3268 | { 3269 | "isbn": "9780393071955", 3270 | "title": "Naked statistics : stripping the dread from the data", 3271 | "author": "Charles J. Wheelan" 3272 | }, 3273 | { 3274 | "isbn": "1851682899", 3275 | "title": "The New Testament : a short introduction : a guide to early Christianity and the Synoptic Gospels", 3276 | "author": "William Telford" 3277 | }, 3278 | { 3279 | "isbn": "0224061658", 3280 | "title": "The occult tradition : from the Renaissance to the present day", 3281 | "author": "David S. Katz" 3282 | }, 3283 | { 3284 | "isbn": "9780062292742", 3285 | "title": "Pope Francis: From the End of the Earth to Rome", 3286 | "author": "The Staff of The Wall Street Journal" 3287 | }, 3288 | { 3289 | "isbn": "0531099474", 3290 | "title": "The Jesuits, a history", 3291 | "author": "David J. Mitchell" 3292 | }, 3293 | { 3294 | "isbn": "1470899000", 3295 | "title": "The Vatican Diaries: A Behind-the-Scenes Look at the Power, Personalities, and Politics at the Heart of the Catholic Church", 3296 | "author": "John Thavis" 3297 | }, 3298 | { 3299 | "isbn": "0486275590", 3300 | "title": "Treasure Island (Dover Thrift Editions)", 3301 | "author": "Robert Louis Stevenson" 3302 | }, 3303 | { 3304 | "isbn": "0486447162", 3305 | "title": "The World's Greatest Short Stories (Dover Thrift Editions)", 3306 | "author": "James Daley" 3307 | }, 3308 | { 3309 | "isbn": "9781400067664", 3310 | "title": "Thomas Jefferson : the art of power", 3311 | "author": "Jon Meacham" 3312 | }, 3313 | { 3314 | "isbn": "1586482734", 3315 | "title": "John F. Kerry : the complete biography by the Boston Globe reporters who know him best", 3316 | "author": "Michael Kranish" 3317 | }, 3318 | { 3319 | "isbn": "9780199588077", 3320 | "title": "Networks : a very short introduction", 3321 | "author": "Guido Caldarelli" 3322 | }, 3323 | { 3324 | "isbn": "0195123018", 3325 | "title": "Monopolies in America : empire builders and their enemies, from Jay Gould to Bill Gates", 3326 | "author": "Charles R. Geisst" 3327 | }, 3328 | { 3329 | "isbn": "9780307592217", 3330 | "title": "The murder of the century : the Gilded Age crime that scandalized a city and sparked the tabloid wars", 3331 | "author": "Paul Collins" 3332 | }, 3333 | { 3334 | "isbn": "0140057463", 3335 | "title": "How far can you go?", 3336 | "author": "David Lodge" 3337 | }, 3338 | { 3339 | "isbn": "158617021X", 3340 | "title": "The Meaning of Tradition", 3341 | "author": "Yves Congar" 3342 | }, 3343 | { 3344 | "isbn": "9780061186479", 3345 | "title": "M is for magic", 3346 | "author": "Neil Gaiman" 3347 | }, 3348 | { 3349 | "isbn": "9780192805805", 3350 | "title": "What makes civilization? : the ancient near east and the future of the west", 3351 | "author": "David Wengrow" 3352 | }, 3353 | { 3354 | "isbn": "1599951509", 3355 | "title": "The Monuments Men: Allied Heroes, Nazi Thieves and the Greatest Treasure Hunt in History", 3356 | "author": "Robert M. Edsel" 3357 | }, 3358 | { 3359 | "isbn": "081463558X", 3360 | "title": "The Social Media Gospel: Sharing the Good News in New Ways", 3361 | "author": "Meredith Gould" 3362 | }, 3363 | { 3364 | "isbn": "9781476733951", 3365 | "title": "Wool", 3366 | "author": "Hugh Howey" 3367 | }, 3368 | { 3369 | "isbn": "0836231007", 3370 | "title": "The making of the Popes 1978 : the politics of intrigue in the Vatican", 3371 | "author": "Andrew M. Greeley" 3372 | }, 3373 | { 3374 | "isbn": "0297844156", 3375 | "title": "Hudson's English history : a compendium", 3376 | "author": "Roger Hudson" 3377 | }, 3378 | { 3379 | "isbn": "9780143106326", 3380 | "title": "The riddle of the sands : a record of secret service", 3381 | "author": "Erskine Childers" 3382 | }, 3383 | { 3384 | "isbn": "9781620400418", 3385 | "title": "What matters in Jane Austen? : twenty crucial puzzles solved", 3386 | "author": "John Mullan" 3387 | }, 3388 | { 3389 | "isbn": "3001160462", 3390 | "title": "What the World is Reading", 3391 | "author": "Penguin Group" 3392 | }, 3393 | { 3394 | "isbn": "0393082067", 3395 | "title": "Intuition Pumps And Other Tools for Thinking", 3396 | "author": "Daniel C. Dennett" 3397 | }, 3398 | { 3399 | "isbn": "0393039250", 3400 | "title": "Why the allies won", 3401 | "author": "R. J. Overy" 3402 | }, 3403 | { 3404 | "isbn": "0983476470", 3405 | "title": "The Other Room", 3406 | "author": "Kim Triedman" 3407 | }, 3408 | { 3409 | "isbn": "0814660118", 3410 | "title": "The Rites of Christian Initiation: Their Evolution and Interpretation (Michael Glazier Books)", 3411 | "author": "Maxwell E. Johnson" 3412 | }, 3413 | { 3414 | "isbn": "075092246X", 3415 | "title": "The last conquistador : Mansio Serra de Lequizamón and the conquest of the Incas", 3416 | "author": "Stuart Stirling" 3417 | }, 3418 | { 3419 | "isbn": "0195040473", 3420 | "title": "Paideia: The Ideals of Greek Culture: Volume II: In Search of the Divine Center", 3421 | "author": "Werner Jaeger" 3422 | }, 3423 | { 3424 | "isbn": "1455520020", 3425 | "title": "Without Their Permission: How the 21st Century Will Be Made, Not Managed", 3426 | "author": "Alexis Ohanian" 3427 | }, 3428 | { 3429 | "isbn": "0385496591", 3430 | "title": "The Lamb's Supper: The Mass as Heaven on Earth", 3431 | "author": "Scott Hahn" 3432 | }, 3433 | { 3434 | "isbn": "0300065159", 3435 | "title": "Understanding Religious Conversion", 3436 | "author": "Lewis R. Rambo" 3437 | }, 3438 | { 3439 | "isbn": "015676248X", 3440 | "title": "Reflections on the Psalms", 3441 | "author": "C. S. Lewis" 3442 | }, 3443 | { 3444 | "isbn": "0394437039", 3445 | "title": "The moviegoer", 3446 | "author": "Walker Percy" 3447 | }, 3448 | { 3449 | "isbn": "0575119519", 3450 | "title": "Riddley Walker (SF Masterworks)", 3451 | "author": "Russell Hoban" 3452 | }, 3453 | { 3454 | "isbn": "0199781729", 3455 | "title": "When God Spoke Greek: The Septuagint and the Making of the Christian Bible", 3456 | "author": "Timothy Michael Law" 3457 | }, 3458 | { 3459 | "isbn": "9780674058071", 3460 | "title": "Latin : story of a world language", 3461 | "author": "Jürgen Leonhardt" 3462 | }, 3463 | { 3464 | "isbn": "0449912183", 3465 | "title": "Roger's Version: A Novel", 3466 | "author": "John Updike" 3467 | }, 3468 | { 3469 | "isbn": "069115290X", 3470 | "title": "Through the Eye of a Needle: Wealth, the Fall of Rome, and the Making of Christianity in the West, 350-550 AD", 3471 | "author": "Peter Brown" 3472 | }, 3473 | { 3474 | "isbn": "0886825016", 3475 | "title": "The ones who walk away from Omelas", 3476 | "author": "Ursula K. Le Guin" 3477 | }, 3478 | { 3479 | "isbn": "0920668372", 3480 | "title": "Love You Forever", 3481 | "author": "Robert Munsch" 3482 | }, 3483 | { 3484 | "isbn": "0307389731", 3485 | "title": "Love in the Time of Cholera (Oprah's Book Club)", 3486 | "author": "Gabriel Garcia Marquez" 3487 | }, 3488 | { 3489 | "isbn": "9791021002596", 3490 | "title": "Test", 3491 | "author": "Test" 3492 | }, 3493 | { 3494 | "isbn": "0809106094", 3495 | "title": "Mercy: The Essence of the Gospel and the Key to Christian Life", 3496 | "author": "Cardinal Walter Kasper" 3497 | }, 3498 | { 3499 | "isbn": "0814680585", 3500 | "title": "Jesus of Nazareth: What He Wanted, Who He Was", 3501 | "author": "Gerhard Lohfink" 3502 | }, 3503 | { 3504 | "isbn": "0742548716", 3505 | "title": "How Do Catholics Read the Bible? (The Come & See Series)", 3506 | "author": "S. J. Daniel J. Harrington" 3507 | }, 3508 | { 3509 | "isbn": "0486445089", 3510 | "title": "Kim (Dover Thrift Editions)", 3511 | "author": "Rudyard Kipling" 3512 | }, 3513 | { 3514 | "isbn": "1878424424", 3515 | "title": "The Mastery of Love: A Practical Guide to the Art of Relationship: A Toltec Wisdom Book", 3516 | "author": "Don Miguel Ruiz" 3517 | }, 3518 | { 3519 | "isbn": "0140440240", 3520 | "title": "The symposium", 3521 | "author": "Plato." 3522 | }, 3523 | { 3524 | "isbn": "0764554336", 3525 | "title": "Physics For Dummies", 3526 | "author": "Steve Holzner" 3527 | }, 3528 | { 3529 | "isbn": "0785250506", 3530 | "title": "Josephus: The Complete Works", 3531 | "author": "Josephus" 3532 | }, 3533 | { 3534 | "isbn": "0590323180", 3535 | "title": "The Mad Scientists' Club", 3536 | "author": "Bertrand R Brinley" 3537 | }, 3538 | { 3539 | "isbn": "0316125407", 3540 | "title": "The Wonderful Flight to the Mushroom Planet", 3541 | "author": "Eleanor Cameron" 3542 | }, 3543 | { 3544 | "isbn": "1591846587", 3545 | "title": "The Launch ||Pad: Inside Y Combinator", 3546 | "author": "Randall Stross" 3547 | }, 3548 | { 3549 | "isbn": "1594161976", 3550 | "title": "The Lost Book of Alexander the Great", 3551 | "author": "Andrew Young" 3552 | }, 3553 | { 3554 | "isbn": "1590170482", 3555 | "title": "The Little Bookroom", 3556 | "author": "Eleanor Farjeon" 3557 | }, 3558 | { 3559 | "isbn": "0765326787", 3560 | "title": "Stand on Zanzibar", 3561 | "author": "John Brunner" 3562 | }, 3563 | { 3564 | "isbn": "0416789501", 3565 | "title": "Tintin and the Lake of Sharks (Tintin Film Book) (The Adventures of Tintin)", 3566 | "author": "A. Herge" 3567 | }, 3568 | { 3569 | "isbn": "0452296293", 3570 | "title": "The Magicians: A Novel (Magicians Trilogy)", 3571 | "author": "Lev Grossman" 3572 | }, 3573 | { 3574 | "isbn": "0792166825", 3575 | "title": "The Virgin Suicides", 3576 | "author": "Kirsten Dunst" 3577 | }, 3578 | { 3579 | "isbn": "0140286829", 3580 | "title": "The Third Man", 3581 | "author": "Graham Greene" 3582 | }, 3583 | { 3584 | "isbn": "0780622251", 3585 | "title": "The Sweet Hereafter", 3586 | "author": "Ian Holm" 3587 | }, 3588 | { 3589 | "isbn": "0140441824", 3590 | "title": "Procopius: The Secret History", 3591 | "author": "Procopius" 3592 | }, 3593 | { 3594 | "isbn": "1840591137", 3595 | "title": "The other side of the mountain", 3596 | "author": "Erendiz Atasü" 3597 | }, 3598 | { 3599 | "isbn": "0671244175", 3600 | "title": "Simon and Schuster's Guide to Rocks and Minerals", 3601 | "author": "Martin Prinz" 3602 | }, 3603 | { 3604 | "isbn": "0446377686", 3605 | "title": "The joy of pigging out", 3606 | "author": "David Hoffman" 3607 | }, 3608 | { 3609 | "isbn": "0790757303", 3610 | "title": "Red planet", 3611 | "author": "Val Kilmer" 3612 | }, 3613 | { 3614 | "isbn": "1400156882", 3615 | "title": "Wuthering Heights (Tantor Unabridged Classics)", 3616 | "author": "Emily Bronte" 3617 | }, 3618 | { 3619 | "isbn": "0790742616", 3620 | "title": "The Matrix", 3621 | "author": "Andy Wachowski" 3622 | }, 3623 | { 3624 | "isbn": "1419873997", 3625 | "title": "Terminator Salvation", 3626 | "author": "" 3627 | }, 3628 | { 3629 | "isbn": "0792188926", 3630 | "title": "The Little Bear Movie", 3631 | "author": "Raymond Jafelice" 3632 | }, 3633 | { 3634 | "isbn": "0792850769", 3635 | "title": "The Princess Bride (Special Edition)", 3636 | "author": "Rob Reiner" 3637 | }, 3638 | { 3639 | "isbn": "1419809792", 3640 | "title": "The invasion", 3641 | "author": "" 3642 | }, 3643 | { 3644 | "isbn": "0783297807", 3645 | "title": "Lost in Translation", 3646 | "author": "Sofia Coppola" 3647 | }, 3648 | { 3649 | "isbn": "0767856120", 3650 | "title": "Stand By Me (Special Edition)", 3651 | "author": "" 3652 | }, 3653 | { 3654 | "isbn": "0783273355", 3655 | "title": "Possession", 3656 | "author": "Neil LaBute" 3657 | }, 3658 | { 3659 | "isbn": "0788826905", 3660 | "title": "Unbreakable (Two-Disc Vista Series)", 3661 | "author": "M. Night Shyamalan" 3662 | }, 3663 | { 3664 | "isbn": "076789880X", 3665 | "title": "Lawrence of Arabia (Single-Disc Edition)", 3666 | "author": "David Lean" 3667 | }, 3668 | { 3669 | "isbn": "0782010032", 3670 | "title": "It's a Wonderful Life", 3671 | "author": "Frank Capra" 3672 | }, 3673 | { 3674 | "isbn": "1566056543", 3675 | "title": "Quadrophenia (Special Edition)", 3676 | "author": "Franc Roddam" 3677 | }, 3678 | { 3679 | "isbn": "0782008372", 3680 | "title": "Highlander: Director's Cut 10th Anniversary Edition", 3681 | "author": "Russell Mulcahy" 3682 | }, 3683 | { 3684 | "isbn": "1580812368", 3685 | "title": "Seven Days In May", 3686 | "author": "Kristin Sergel" 3687 | }, 3688 | { 3689 | "isbn": "0788604546", 3690 | "title": "The Great Communicator (Complete Set)", 3691 | "author": "Ronald Reagan" 3692 | }, 3693 | { 3694 | "isbn": "141703050X", 3695 | "title": "Serenity (Widescreen Edition)", 3696 | "author": "Joss Whedon" 3697 | } 3698 | ] 3699 | } --------------------------------------------------------------------------------