hlquery-node-client 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +37 -0
- package/LICENSE +29 -0
- package/README.md +316 -0
- package/example.js +636 -0
- package/examples/basic_usage.js +35 -0
- package/examples/collections.js +40 -0
- package/examples/csv.js +39 -0
- package/examples/documents.js +54 -0
- package/examples/flush.js +128 -0
- package/examples/keys.js +58 -0
- package/examples/pdf.js +40 -0
- package/examples/search.js +101 -0
- package/examples/vector.js +465 -0
- package/index.js +39 -0
- package/lib/Aliases.js +48 -0
- package/lib/Client.js +623 -0
- package/lib/Collections.js +159 -0
- package/lib/Documents.js +244 -0
- package/lib/Exceptions.js +101 -0
- package/lib/Keys.js +85 -0
- package/lib/Overrides.js +53 -0
- package/lib/Request.js +147 -0
- package/lib/Response.js +54 -0
- package/lib/Search.js +350 -0
- package/lib/Stopwords.js +60 -0
- package/lib/Synonyms.js +85 -0
- package/lib/System.js +110 -0
- package/lib/conformance.js +74 -0
- package/package.json +42 -0
- package/test/offline-smoke.js +86 -0
- package/utils/Auth.js +34 -0
- package/utils/CSVParser.js +160 -0
- package/utils/Config.js +57 -0
- package/utils/PDFParser.js +196 -0
- package/utils/Validator.js +143 -0
- package/utils/ranker.js +51 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hlquery Node.js Client - Optional PDF parsing helper
|
|
3
|
+
*
|
|
4
|
+
* Copyright (C) 2021-2026, Carlos F. Ferry <carlos.ferry@gmail.com>
|
|
5
|
+
*
|
|
6
|
+
* This file is part of hlquery, released under the BSD License version 3.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const { ValidationException } = require('../lib/Exceptions');
|
|
13
|
+
|
|
14
|
+
class PDFParser {
|
|
15
|
+
/**
|
|
16
|
+
* Parse a PDF file from disk.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} filePath
|
|
19
|
+
* @param {object} options
|
|
20
|
+
* @returns {Promise<object>}
|
|
21
|
+
*/
|
|
22
|
+
static async parseFile(filePath, options = {}) {
|
|
23
|
+
this.validateFilePath(filePath);
|
|
24
|
+
|
|
25
|
+
const resolvedPath = path.resolve(filePath);
|
|
26
|
+
const stat = fs.statSync(resolvedPath);
|
|
27
|
+
|
|
28
|
+
if (!stat.isFile()) {
|
|
29
|
+
throw new ValidationException(`PDF path is not a file: ${resolvedPath}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pdfParse = this.loadPDFParse();
|
|
33
|
+
const buffer = fs.readFileSync(resolvedPath);
|
|
34
|
+
const parsed = await pdfParse(buffer);
|
|
35
|
+
const metadata = parsed && parsed.metadata && typeof parsed.metadata === 'object'
|
|
36
|
+
? parsed.metadata
|
|
37
|
+
: {};
|
|
38
|
+
|
|
39
|
+
const content = this.normalizeText(parsed && parsed.text ? parsed.text : '', options);
|
|
40
|
+
const title = this.resolveTitle(resolvedPath, metadata, options);
|
|
41
|
+
const fileName = path.basename(resolvedPath);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
filePath: resolvedPath,
|
|
45
|
+
fileName: fileName,
|
|
46
|
+
title: title,
|
|
47
|
+
content: content,
|
|
48
|
+
pageCount: parsed && typeof parsed.numpages === 'number' ? parsed.numpages : null,
|
|
49
|
+
info: parsed && parsed.info ? parsed.info : {},
|
|
50
|
+
metadata: metadata,
|
|
51
|
+
version: parsed && parsed.version ? parsed.version : null,
|
|
52
|
+
sizeBytes: stat.size
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build a standard hlquery document from parsed PDF data.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} parsed
|
|
60
|
+
* @param {object} options
|
|
61
|
+
* @returns {object}
|
|
62
|
+
*/
|
|
63
|
+
static buildDocumentFromParsedPDF(parsed, options = {}) {
|
|
64
|
+
const contentField = options.contentField || 'content';
|
|
65
|
+
const titleField = options.titleField || 'title';
|
|
66
|
+
const document = Object.assign({}, options.document || {});
|
|
67
|
+
|
|
68
|
+
document.id = options.id || document.id || this.generateDocumentId(parsed.fileName);
|
|
69
|
+
document[titleField] = options.title || document[titleField] || parsed.title;
|
|
70
|
+
document[contentField] = parsed.content;
|
|
71
|
+
|
|
72
|
+
if (options.includeMetadata !== false) {
|
|
73
|
+
document.file_name = parsed.fileName;
|
|
74
|
+
document.file_path = parsed.filePath;
|
|
75
|
+
document.mime_type = 'application/pdf';
|
|
76
|
+
|
|
77
|
+
if (parsed.pageCount !== null) {
|
|
78
|
+
document.page_count = String(parsed.pageCount);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (parsed.sizeBytes !== null) {
|
|
82
|
+
document.file_size_bytes = String(parsed.sizeBytes);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.attachMetadata(document, parsed.info, 'pdf_info', options);
|
|
86
|
+
this.attachMetadata(document, parsed.metadata, 'pdf_meta', options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return document;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static attachMetadata(document, metadata, prefix, options = {}) {
|
|
93
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fieldPrefix = options.metadataPrefix || prefix;
|
|
98
|
+
|
|
99
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
100
|
+
if (value === null || value === undefined) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const normalizedKey = this.normalizeFieldName(`${fieldPrefix}_${key}`);
|
|
105
|
+
const normalizedValue = this.normalizeMetadataValue(value, options);
|
|
106
|
+
|
|
107
|
+
if (normalizedKey && normalizedValue !== '') {
|
|
108
|
+
document[normalizedKey] = normalizedValue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static normalizeMetadataValue(value, options = {}) {
|
|
114
|
+
if (typeof value === 'string') {
|
|
115
|
+
return this.normalizeText(value, options);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
119
|
+
return String(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return this.normalizeText(JSON.stringify(value), options);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static normalizeFieldName(name) {
|
|
126
|
+
return String(name)
|
|
127
|
+
.trim()
|
|
128
|
+
.toLowerCase()
|
|
129
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
130
|
+
.replace(/^_+|_+$/g, '')
|
|
131
|
+
.slice(0, 64);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static normalizeText(text, options = {}) {
|
|
135
|
+
let output = String(text || '');
|
|
136
|
+
|
|
137
|
+
output = output.replace(/\r\n/g, '\n');
|
|
138
|
+
|
|
139
|
+
if (options.stripNullBytes !== false) {
|
|
140
|
+
output = output.replace(/\0/g, '');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (options.sanitizeCommas !== false) {
|
|
144
|
+
output = output.replace(/,/g, ' ');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (options.collapseWhitespace !== false) {
|
|
148
|
+
output = output.replace(/[ \t]+/g, ' ');
|
|
149
|
+
output = output.replace(/\n{3,}/g, '\n\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return output.trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static resolveTitle(filePath, metadata, options = {}) {
|
|
156
|
+
if (options.title) {
|
|
157
|
+
return this.normalizeText(options.title, options);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (metadata && typeof metadata.Title === 'string' && metadata.Title.trim() !== '') {
|
|
161
|
+
return this.normalizeText(metadata.Title, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return this.normalizeText(path.basename(filePath, path.extname(filePath)), options);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static generateDocumentId(fileName) {
|
|
168
|
+
const stem = path.basename(fileName, path.extname(fileName))
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
.replace(/[^a-z0-9._-]+/g, '_')
|
|
171
|
+
.replace(/^_+|_+$/g, '')
|
|
172
|
+
.slice(0, 40);
|
|
173
|
+
const suffix = crypto.randomBytes(4).toString('hex');
|
|
174
|
+
|
|
175
|
+
return `${stem || 'pdf'}_${suffix}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static validateFilePath(filePath) {
|
|
179
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
180
|
+
throw new ValidationException('PDF file path must be a non-empty string');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static loadPDFParse() {
|
|
185
|
+
try {
|
|
186
|
+
return require('pdf-parse');
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new ValidationException(
|
|
189
|
+
'PDF parsing requires the optional dependency `pdf-parse`. ' +
|
|
190
|
+
'Install it in etc/api/node with `npm install pdf-parse`.'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = PDFParser;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hlquery Node.js Client - Input Validation
|
|
3
|
+
*
|
|
4
|
+
* Copyright (C) 2021-2026, Carlos F. Ferry <carlos.ferry@gmail.com>
|
|
5
|
+
*
|
|
6
|
+
* This file is part of hlquery, released under the BSD License version 3.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { ValidationException } = require('../lib/Exceptions');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Input validation utilities
|
|
13
|
+
*/
|
|
14
|
+
class Validator {
|
|
15
|
+
/**
|
|
16
|
+
* Validate collection name
|
|
17
|
+
*/
|
|
18
|
+
static validateCollectionName(name) {
|
|
19
|
+
if (!name || typeof name !== 'string' || name.trim() === '') {
|
|
20
|
+
throw new ValidationException('Collection name must be a non-empty string');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check name length (matches server validation: 1-64 characters)
|
|
24
|
+
if (name.length > 64) {
|
|
25
|
+
throw new ValidationException('Collection name must be between 1 and 64 characters');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Collection names should be URL-safe
|
|
29
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
30
|
+
throw new ValidationException(
|
|
31
|
+
'Collection name contains invalid characters. ' +
|
|
32
|
+
'Use only letters, numbers, underscores, and hyphens'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if name starts with letter or underscore (matches server validation)
|
|
37
|
+
const firstChar = name[0];
|
|
38
|
+
if (!/[a-zA-Z]/.test(firstChar) && firstChar !== '_') {
|
|
39
|
+
throw new ValidationException('Collection name must start with a letter or underscore');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate document ID
|
|
45
|
+
*/
|
|
46
|
+
static validateDocumentId(id) {
|
|
47
|
+
if (!id || (typeof id !== 'string' && typeof id !== 'number')) {
|
|
48
|
+
throw new ValidationException('Document ID must be a non-empty string or number');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Convert to string for validation
|
|
52
|
+
const idStr = String(id);
|
|
53
|
+
|
|
54
|
+
// Check name length (matches server validation: 1-64 characters)
|
|
55
|
+
if (idStr.length > 64) {
|
|
56
|
+
throw new ValidationException('Document ID must be between 1 and 64 characters');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Document IDs should be URL-safe: alphanumeric, underscores, and hyphens only
|
|
60
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(idStr)) {
|
|
61
|
+
throw new ValidationException(
|
|
62
|
+
'Document ID contains invalid characters. ' +
|
|
63
|
+
'Use only letters, numbers, underscores, and hyphens'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate pagination parameters
|
|
70
|
+
*/
|
|
71
|
+
static validatePagination(offset, limit) {
|
|
72
|
+
if (offset !== undefined && (typeof offset !== 'number' || offset < 0)) {
|
|
73
|
+
throw new ValidationException('Offset must be a non-negative number');
|
|
74
|
+
}
|
|
75
|
+
if (limit !== undefined && (typeof limit !== 'number' || limit < 1)) {
|
|
76
|
+
throw new ValidationException('Limit must be a positive number');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate search parameters
|
|
82
|
+
*/
|
|
83
|
+
static validateSearchParams(params) {
|
|
84
|
+
if (params && typeof params !== 'object') {
|
|
85
|
+
throw new ValidationException('Search parameters must be an object');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (params) {
|
|
89
|
+
if (params.offset !== undefined && (typeof params.offset !== 'number' || params.offset < 0)) {
|
|
90
|
+
throw new ValidationException('Offset must be a non-negative number');
|
|
91
|
+
}
|
|
92
|
+
if (params.limit !== undefined && (typeof params.limit !== 'number' || params.limit < 1)) {
|
|
93
|
+
throw new ValidationException('Limit must be a positive number');
|
|
94
|
+
}
|
|
95
|
+
if (params.from !== undefined && (typeof params.from !== 'number' || params.from < 0)) {
|
|
96
|
+
throw new ValidationException('From must be a non-negative number');
|
|
97
|
+
}
|
|
98
|
+
if (params.size !== undefined && (typeof params.size !== 'number' || params.size < 1)) {
|
|
99
|
+
throw new ValidationException('Size must be a positive number');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate document field values for invalid characters
|
|
106
|
+
* Commas are not allowed in string field values as they're reserved for internal parsing
|
|
107
|
+
*
|
|
108
|
+
* @param {object} document - Document object to validate
|
|
109
|
+
* @throws {ValidationException} If document contains invalid characters
|
|
110
|
+
*/
|
|
111
|
+
static validateDocumentFields(document) {
|
|
112
|
+
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
113
|
+
return; // Skip validation for non-objects or arrays (will be validated per-item)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const [key, value] of Object.entries(document)) {
|
|
117
|
+
// Skip the 'id' field as it has its own validation
|
|
118
|
+
if (key === 'id') continue;
|
|
119
|
+
|
|
120
|
+
// Check string values for commas
|
|
121
|
+
if (typeof value === 'string' && value.includes(',')) {
|
|
122
|
+
throw new ValidationException(
|
|
123
|
+
`Field '${key}' contains invalid character: comma (`,`). ` +
|
|
124
|
+
`Commas are not allowed in field values. Use underscores (_) or spaces instead, or use arrays for multiple values.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check array values - ensure they don't contain strings with commas
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
for (const item of value) {
|
|
131
|
+
if (typeof item === 'string' && item.includes(',')) {
|
|
132
|
+
throw new ValidationException(
|
|
133
|
+
`Field '${key}' contains invalid character: comma (`,`). ` +
|
|
134
|
+
`Array items cannot contain commas. Use underscores (_) or spaces instead.`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = Validator;
|
package/utils/ranker.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hlquery Node.js Client - Ranking Helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const DEFAULT_WEIGHTS = {
|
|
6
|
+
popularity_log: 1.15,
|
|
7
|
+
hit_log: 0.95,
|
|
8
|
+
popularity_sqrt: 0.25,
|
|
9
|
+
hit_log_sqrt: 0.15,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function computeRankSignal(popularity, hitLog, weights = {}) {
|
|
13
|
+
const w = { ...DEFAULT_WEIGHTS };
|
|
14
|
+
|
|
15
|
+
for (const key of Object.keys(weights)) {
|
|
16
|
+
if (Object.prototype.hasOwnProperty.call(w, key) && typeof weights[key] === 'number') {
|
|
17
|
+
w[key] = weights[key];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
Math.log(popularity + 1) * w.popularity_log
|
|
23
|
+
+ Math.log(hitLog + 1) * w.hit_log
|
|
24
|
+
+ Math.sqrt(popularity) * w.popularity_sqrt
|
|
25
|
+
+ Math.sqrt(hitLog) * w.hit_log_sqrt
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function attachRankSort(params, field = 'rank_signal', direction = 'desc') {
|
|
30
|
+
if (!params || typeof params !== 'object') {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
direction = direction.toLowerCase();
|
|
35
|
+
if (direction !== 'asc' && direction !== 'desc') {
|
|
36
|
+
direction = 'desc';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const sortInstruction = `${field}:${direction}`;
|
|
40
|
+
|
|
41
|
+
if (typeof params.sort_by === 'string' && params.sort_by.trim()) {
|
|
42
|
+
params.sort_by = `${params.sort_by},${sortInstruction}`;
|
|
43
|
+
} else {
|
|
44
|
+
params.sort_by = sortInstruction;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
computeRankSignal,
|
|
50
|
+
attachRankSort
|
|
51
|
+
};
|