solr-client
Version:
A Solr client library for indexing, adding, deleting, committing, optimizing and searching documents within an Apache Solr installation (version>=3.2)
484 lines • 16.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = exports.resolvePort = exports.createClient = void 0;
const querystring = require("querystring");
const JSONStream = require("JSONStream");
const duplexer = require("duplexer");
const query_1 = require("./query");
const collection_1 = require("./collection");
const versionUtils = require("./utils/version");
const undici_1 = require("undici");
const oldRequest = require('request');
const format = require('./utils/format');
const JSONbig = require('json-bigint');
/**
* Pick appropriate JSON serializer/deserializer library based on the given `bigint` flag
*
* @param bigint
* Whether to handle big numbers correctly or not.
* The reason for not using JSONbig all the times is it has a significant performance cost.
*
* @return
* JSON or JSONbig serializer/deserializer
*/
function pickJSON(bigint) {
return bigint ? JSONbig : JSON;
}
function createClient(options = {}) {
return new Client(options);
}
exports.createClient = createClient;
function resolvePort(options) {
if (options.port === undefined) {
return '8983';
}
if (options.port === null || options.port === '') {
return '';
}
return options.port;
}
exports.resolvePort = resolvePort;
/**
* Solr client.
*/
class Client {
constructor(options = {}) {
/**
* Updates a document or a list of documents.
*
* This function is a clone of `add` and it was added to give more clarity on the availability of atomic updates.
*/
this.atomicUpdate = Client.prototype.add;
/**
* Expose `format.escapeSpecialChars` from `Client.escapeSpecialChars`
*/
this.escapeSpecialChars = format.escapeSpecialChars;
this.options = {
host: options.host || '127.0.0.1',
port: resolvePort(options),
core: options.core || '',
path: options.path || '/solr',
tls: options.tls,
secure: options.secure || false,
bigint: options.bigint || false,
get_max_request_entity_size: options.get_max_request_entity_size || false,
solrVersion: options.solrVersion || versionUtils.Solr3_2,
ipVersion: options.ipVersion == 6 ? 6 : 4,
request: options.request || null,
};
// Default paths of all request handlers
this.UPDATE_JSON_HANDLER =
versionUtils.version(this.options.solrVersion) >= versionUtils.Solr4_0
? 'update'
: 'update/json';
this.UPDATE_HANDLER = 'update';
this.SELECT_HANDLER = 'select';
this.COLLECTIONS_HANDLER = 'admin/collections';
this.ADMIN_PING_HANDLER = 'admin/ping';
this.REAL_TIME_GET_HANDLER = 'get';
this.SPELL_HANDLER = 'spell';
this.TERMS_HANDLER = 'terms';
const urlPrefix = this.options.host.startsWith('http')
? ''
: this.options.tls
? 'https://'
: 'http://';
this.undiciClient = new undici_1.Client(`${urlPrefix}${this.options.host}:${this.options.port}`, {
connect: this.options.tls,
});
}
get solrVersion() {
return this.options.solrVersion;
}
/**
* Construct the full path to the given "handler".
*
* @param handler
* Relative URL path for the solr handler.
*
* @returns
* Full URL to the handler.
*/
getFullHandlerPath(handler) {
let pathArray;
if (handler === this.COLLECTIONS_HANDLER) {
pathArray = [this.options.path, handler];
}
else {
pathArray = [this.options.path, this.options.core, handler];
}
return pathArray.filter((e) => e).join('/');
}
/**
* Common function for all HTTP requests.
*
* @param path
* Full URL for the request.
* @param method
* HTTP method, like "GET" or "POST".
* @param body
* Optional data for the request body.
* @param bodyContentType
* Optional content type for the request body.
* @param acceptContentType
* The expected content type of the response.
*
* @returns
* Parsed JSON response data.
*/
async doRequest(path, method, body, bodyContentType, acceptContentType) {
const protocol = this.options.secure ? 'https' : 'http';
const url = `${protocol}://${this.options.host}:${this.options.port}${path}`;
const requestOptions = {
...this.options.request,
method,
};
// Now set options that the user should not be able to override.
if (!requestOptions.headers) {
requestOptions.headers = {};
}
requestOptions.headers['accept'] = acceptContentType;
if (method === 'POST') {
if (bodyContentType) {
requestOptions.headers['content-type'] = bodyContentType;
}
if (body) {
requestOptions.headers['content-length'] = Buffer.byteLength(body);
requestOptions.body = body;
}
}
if (this.options.authorization) {
requestOptions.headers['authorization'] = this.options.authorization;
}
// Perform the request and handle results.
const response = await this.undiciClient.request({
path: url,
...requestOptions,
});
// Always consume the response body. See https://github.com/nodejs/undici#garbage-collection
const text = await response.body.text();
// Undici does not throw an error on certain status codes, this leaves that to us
if (response.statusCode < 200 || response.statusCode > 299) {
throw new Error(`Request HTTP error ${response.statusCode}: ${text}`);
}
return pickJSON(this.options.bigint).parse(text);
}
/**
* Create credential using the basic access authentication method
*/
basicAuth(username, password) {
this.options.authorization =
'Basic ' + Buffer.from(username + ':' + password).toString('base64');
return this;
}
/**
* Remove authorization header
*/
unauth() {
delete this.options.authorization;
return this;
}
/**
* Add a document or a list of documents.
*
* @param docs
* Document or list of documents to add into the Solr database.
* @param queryParameters
* Query parameters to include in the URL.
*/
add(docs, queryParameters) {
// format `Date` object into string understood by Solr as a date.
docs = format.dateISOify(docs);
docs = Array.isArray(docs) ? docs : [docs];
return this.update(docs, queryParameters);
}
/**
* Get a document by id or a list of documents by ids using the Real-time-get feature
* in SOLR4 (https://wiki.apache.org/solr/RealTimeGet)
*
* @param ids
* ID or list of IDs that identify the documents to get.
* @param query
*/
realTimeGet(ids, query = {}) {
ids = Array.isArray(ids) ? ids : [ids];
if (typeof query === 'object') {
query['ids'] = ids.join(',');
}
return this.doQuery(this.REAL_TIME_GET_HANDLER, query);
}
/**
* Add the remote resource located at the given path `options.path` into
* the Solr database.
*/
addRemoteResource(options) {
options.parameters = options.parameters || {};
options.format = options.format === 'xml' ? '' : options.format || ''; // reason: the default route of the XmlUpdateRequestHandle is /update and not /update/xml.
options.parameters.commit =
options.parameters.commit === undefined
? false
: options.parameters.commit;
options.parameters['stream.contentType'] =
options.contentType || 'text/plain;charset=utf-8';
if (options.path.match(/^https?:\/\//)) {
options.parameters['stream.url'] = options.path;
}
else {
options.parameters['stream.file'] = options.path;
}
const handler = this.UPDATE_HANDLER + '/' + options.format.toLowerCase();
const query = querystring.stringify(options.parameters);
return this.doQuery(handler, query);
}
/**
* Create a writable/readable `Stream` to add documents into the Solr database.
*/
createAddStream(options = {}) {
const path = [
this.options.path,
this.options.core,
this.UPDATE_JSON_HANDLER +
'?' +
querystring.stringify({ ...options, wt: 'json' }),
]
.filter(function (element) {
return element;
})
.join('/');
const headers = {
'content-type': 'application/json',
charset: 'utf-8',
};
if (this.options.authorization) {
headers['authorization'] = this.options.authorization;
}
const protocol = this.options.secure ? 'https' : 'http';
const optionsRequest = {
url: protocol + '://' + this.options.host + ':' + this.options.port + path,
method: 'POST',
headers: headers,
};
const jsonStreamStringify = JSONStream.stringify();
const postRequest = oldRequest(optionsRequest);
jsonStreamStringify.pipe(postRequest);
return duplexer(jsonStreamStringify, postRequest);
}
/**
* Commit last added and removed documents, that means your documents are now indexed.
*/
commit(options) {
return this.update({
commit: options || {},
});
}
/**
* Call Lucene's IndexWriter.prepareCommit, the changes won't be visible in the index.
*/
prepareCommit() {
return this.update({}, { prepareCommit: true });
}
/**
* Soft commit all changes
*/
softCommit() {
return this.update({}, { softCommit: true });
}
/**
* Delete documents based on the given `field` and `text`.
*/
delete(field, text, options) {
return this.update({
delete: {
query: field + ':' + format.escapeSpecialChars(format.dateISOify(text)),
},
}, options);
}
/**
* Delete a range of documents based on the given `field`, `start` and `stop` arguments.
*/
deleteByRange(field, start, stop, options) {
start = format.dateISOify(start);
stop = format.dateISOify(stop);
return this.deleteByQuery(`${field}:[${start} TO ${stop}]`, options);
}
/**
* Delete the document with the given `id`
*
* @param id
* ID of the document you want to delete.
* @param options
*/
deleteByID(id, options) {
return this.update({
delete: {
id: id,
},
}, options);
}
/**
* Delete documents matching the given `query`.
*/
deleteByQuery(query, options) {
return this.update({
delete: {
query: query,
},
}, options);
}
/**
* Delete all documents.
*/
deleteAll(options) {
return this.deleteByQuery('*:*', options);
}
/**
* Optimize the index.
*/
optimize(options) {
return this.update({
optimize: options || {},
});
}
/**
* Rollback all add/delete commands made since the last commit.
*/
rollback() {
return this.update({
rollback: {},
});
}
/**
* Send an update command to the Solr server.
*
* @param data
* The data to stringify in the body.
* @param queryParameters
* Query parameters to include in the URL.
*/
update(data, queryParameters) {
const path = this.getFullHandlerPath(this.UPDATE_JSON_HANDLER);
const queryString = querystring.stringify({
...queryParameters,
wt: 'json',
});
return this.doRequest(`${path}?${queryString}`, 'POST', pickJSON(this.options.bigint).stringify(data), 'application/json', 'application/json; charset=utf-8');
}
/**
* Search documents matching the `query`
*/
search(query) {
return this.doQuery(this.SELECT_HANDLER, query);
}
/**
* Execute an Admin Collections task on `collection`
*/
executeCollection(collection) {
return this.doQuery(this.COLLECTIONS_HANDLER, collection);
}
/**
* Search for all documents.
*/
searchAll() {
return this.search('q=*');
}
/**
* Search documents matching the `query`, with spellchecking enabled.
*/
spell(query) {
return this.doQuery(this.SPELL_HANDLER, query);
}
/**
* Terms search.
*
* Provides access to the indexed terms in a field and the number of documents that match each term.
*/
termsSearch(query) {
return this.doQuery(this.TERMS_HANDLER, query);
}
/**
* Perform an arbitrary query on a Solr handler (a.k.a. 'path').
*
* @param handler
* The name of the handler (or 'path' in Solr terminology).
* @param query
* A function, Query object, Collection object, plain object, or string
* describing the query to perform.
*/
async doQuery(handler, query) {
// Construct the string to use as query (GET) or body (POST).
let data;
if (query instanceof query_1.Query || query instanceof collection_1.Collection) {
data = query.build();
}
else if (typeof query === 'object') {
data = querystring.stringify(query);
}
else {
// query is a string.
data = query;
}
const path = this.getFullHandlerPath(handler);
const queryString = data ? data + '&wt=json' : 'wt=json';
// Decide whether to use GET or POST, based on the length of the data.
// 10 accounts for protocol and special characters like ://, port colon,
// initial slash, etc.
const approxUrlLength = 10 +
Buffer.byteLength(this.options.host) +
this.options.port.toString().length +
Buffer.byteLength(path) +
1 +
Buffer.byteLength(queryString);
const method = this.options.get_max_request_entity_size === false ||
approxUrlLength <= this.options.get_max_request_entity_size
? 'GET'
: 'POST';
return this.doRequest(method === 'GET' ? `${path}?${queryString}` : path, method, method === 'POST' ? data : null, method === 'POST'
? 'application/x-www-form-urlencoded; charset=utf-8'
: null, 'application/json; charset=utf-8');
}
/**
* Create an instance of `Query`
*/
query() {
return new query_1.Query(this.options);
}
/**
* Create an instance of `Collection`.
*/
collection() {
return new collection_1.Collection();
}
/**
* Ping the Solr server.
*/
ping() {
return this.doQuery(this.ADMIN_PING_HANDLER, '');
}
/**
* Utility only used in tests.
*
* @param {string} fieldName
* The name of the field to create.
* @param {string} fieldType
* The type of field to create.
*/
createSchemaField(fieldName, fieldType) {
return this.doRequest(this.getFullHandlerPath('schema'), 'POST', pickJSON(this.options.bigint).stringify({
'add-field': {
name: fieldName,
type: fieldType,
multiValued: false,
stored: true,
},
}), 'application/json', 'application/json; charset=utf-8').catch((err) => {
// TODO: We should handle this in a more robust way in the future, but
// there is a difference between default setup in Solr 5 and Solr 8,
// so some fields already exist in Solr 8. Hence if that's the case,
// we just ignore that.
console.warn(err.message);
return {};
});
}
}
exports.Client = Client;
//# sourceMappingURL=solr.js.map