152 lines
4.6 KiB
JavaScript
152 lines
4.6 KiB
JavaScript
/**
|
|
* Map Obsidian-style search queries to Meilisearch format
|
|
* Supports: tag:, path:, file: operators with free text search
|
|
*/
|
|
export function mapObsidianQueryToMeili(qRaw) {
|
|
const tokens = String(qRaw).trim().split(/\s+/);
|
|
const filters = [];
|
|
const meiliQ = [];
|
|
let restrict = null;
|
|
let rangeStart = null;
|
|
let rangeEnd = null;
|
|
|
|
const parseDate = (s) => {
|
|
if (!s) return null;
|
|
// Accept YYYY-MM-DD
|
|
const m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
if (!m) return null;
|
|
const d = new Date(`${m[1]}-${m[2]}-${m[3]}T00:00:00Z`);
|
|
if (Number.isNaN(d.getTime())) return null;
|
|
return d;
|
|
};
|
|
|
|
for (const token of tokens) {
|
|
// tag: operator - filter by exact tag match
|
|
if (token.startsWith('tag:')) {
|
|
const value = token.slice(4).replace(/^["']|["']$/g, '');
|
|
if (value) {
|
|
// Remove leading # if present
|
|
const cleanTag = value.startsWith('#') ? value.substring(1) : value;
|
|
filters.push(`tags = "${cleanTag}"`);
|
|
}
|
|
}
|
|
// path: operator - filter by parent directory path
|
|
// We use parentDirs array to simulate startsWith behavior
|
|
else if (token.startsWith('path:')) {
|
|
const value = token.slice(5).replace(/^["']|["']$/g, '');
|
|
if (value) {
|
|
// Normalize path separators
|
|
const normalizedPath = value.replace(/\\/g, '/');
|
|
filters.push(`parentDirs = "${normalizedPath}"`);
|
|
}
|
|
}
|
|
// file: operator - restrict search to file field
|
|
else if (token.startsWith('file:')) {
|
|
const value = token.slice(5).replace(/^["']|["']$/g, '');
|
|
if (value) {
|
|
// Restrict search to file field only
|
|
restrict = ['file'];
|
|
meiliQ.push(value);
|
|
}
|
|
}
|
|
// year: operator - facet filter
|
|
else if (token.startsWith('year:')) {
|
|
const value = token.slice(5).replace(/^["']|["']$/g, '');
|
|
const n = Number(value);
|
|
if (!Number.isNaN(n)) {
|
|
filters.push(`year = ${n}`);
|
|
}
|
|
}
|
|
// month: operator - facet filter (1-12)
|
|
else if (token.startsWith('month:')) {
|
|
const value = token.slice(6).replace(/^["']|["']$/g, '');
|
|
const n = Number(value);
|
|
if (!Number.isNaN(n)) {
|
|
filters.push(`month = ${n}`);
|
|
}
|
|
}
|
|
// date:YYYY-MM-DD (single day)
|
|
else if (token.startsWith('date:')) {
|
|
const value = token.slice(5);
|
|
const d = parseDate(value);
|
|
if (d) {
|
|
const start = new Date(d); start.setUTCHours(0,0,0,0);
|
|
const end = new Date(d); end.setUTCHours(23,59,59,999);
|
|
const startMs = start.getTime();
|
|
const endMs = end.getTime();
|
|
filters.push(`((createdAt >= ${startMs} AND createdAt <= ${endMs}) OR (updatedAt >= ${startMs} AND updatedAt <= ${endMs}))`);
|
|
}
|
|
}
|
|
// from:/to: YYYY-MM-DD
|
|
else if (token.startsWith('from:')) {
|
|
const d = parseDate(token.slice(5));
|
|
if (d) {
|
|
const s = new Date(d); s.setUTCHours(0,0,0,0);
|
|
rangeStart = s.getTime();
|
|
}
|
|
}
|
|
else if (token.startsWith('to:')) {
|
|
const d = parseDate(token.slice(3));
|
|
if (d) {
|
|
const e = new Date(d); e.setUTCHours(23,59,59,999);
|
|
rangeEnd = e.getTime();
|
|
}
|
|
}
|
|
// Regular text token
|
|
else if (token) {
|
|
meiliQ.push(token);
|
|
}
|
|
}
|
|
|
|
// If we captured a from/to range, add a combined date filter
|
|
if (rangeStart !== null || rangeEnd !== null) {
|
|
const startMs = rangeStart ?? 0;
|
|
const endMs = rangeEnd ?? 8640000000000000; // large future
|
|
filters.push(`((createdAt >= ${startMs} AND createdAt <= ${endMs}) OR (updatedAt >= ${startMs} AND updatedAt <= ${endMs}))`);
|
|
}
|
|
|
|
return {
|
|
meiliQ: meiliQ.join(' ').trim(),
|
|
filters,
|
|
restrict
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build Meilisearch search parameters from parsed query
|
|
*/
|
|
export function buildSearchParams(parsedQuery, options = {}) {
|
|
const {
|
|
limit = 20,
|
|
offset = 0,
|
|
sort,
|
|
highlight = true
|
|
} = options;
|
|
|
|
const params = {
|
|
q: parsedQuery.meiliQ,
|
|
limit: Number(limit),
|
|
offset: Number(offset),
|
|
filter: parsedQuery.filters.length ? parsedQuery.filters.join(' AND ') : undefined,
|
|
facets: ['tags', 'parentDirs', 'year', 'month'],
|
|
attributesToHighlight: highlight ? ['title', 'content'] : [],
|
|
highlightPreTag: '<mark>',
|
|
highlightPostTag: '</mark>',
|
|
attributesToCrop: ['content'],
|
|
cropLength: 80,
|
|
cropMarker: '…',
|
|
attributesToSearchOn: parsedQuery.restrict?.length ? parsedQuery.restrict : undefined,
|
|
sort: sort ? [String(sort)] : undefined,
|
|
showMatchesPosition: false
|
|
};
|
|
|
|
// Remove undefined values
|
|
Object.keys(params).forEach(key => {
|
|
if (params[key] === undefined) {
|
|
delete params[key];
|
|
}
|
|
});
|
|
|
|
return params;
|
|
}
|