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;
 | 
						|
}
 |