code icon Code

Search JSON Array

Search array items by text matching in specified fields

Source Code

import fs from "fs";
import path from "path";

const [inputPath, query, fieldsArg, mode = "contains", outputPath] =
  process.argv.slice(2);

if (!inputPath || !query || !fieldsArg || !outputPath) {
  console.error("Usage: inputPath query fields [mode] outputPath");
  process.exit(1);
}

/**
 * Get nested field value using dot notation
 */
function getField(obj, fieldPath) {
  const parts = fieldPath.split(".");
  let value = obj;
  for (const part of parts) {
    if (value == null) return undefined;
    value = value[part];
  }
  return value;
}

/**
 * Calculate simple fuzzy match score (Levenshtein-like)
 */
function fuzzyScore(str, query) {
  const s = str.toLowerCase();
  const q = query.toLowerCase();

  // Check for substring match first
  if (s.includes(q)) return 1;

  // Check if all query characters appear in order
  let sIdx = 0;
  let matchCount = 0;
  for (const char of q) {
    const found = s.indexOf(char, sIdx);
    if (found >= 0) {
      matchCount++;
      sIdx = found + 1;
    }
  }

  // Return ratio of matched characters
  return matchCount / q.length;
}

/**
 * Check if item matches the search query
 */
function matches(item, fields, query, mode) {
  const queryLower = query.toLowerCase();

  for (const field of fields) {
    const value = getField(item, field);
    if (value == null) continue;

    const strValue = String(value);
    const strLower = strValue.toLowerCase();

    switch (mode) {
      case "exact":
        if (strLower === queryLower) return true;
        break;

      case "contains":
        if (strLower.includes(queryLower)) return true;
        break;

      case "fuzzy":
        if (fuzzyScore(strValue, query) >= 0.7) return true;
        break;
    }
  }

  return false;
}

try {
  console.log(`Reading ${inputPath}...`);
  const raw = fs.readFileSync(inputPath, "utf-8");
  const data = JSON.parse(raw);

  const items = Array.isArray(data)
    ? data
    : data.items || data.results || data.messages || [];

  if (!Array.isArray(items)) {
    console.error("Input must be a JSON array or object with array property");
    process.exit(1);
  }

  const fields = fieldsArg.split(",").map((f) => f.trim());

  console.log(
    `Searching ${items.length} items for "${query}" in fields: ${fields.join(", ")} (${mode} mode)...`
  );

  const results = items.filter((item) => matches(item, fields, query, mode));

  // Ensure output directory exists
  const dir = path.dirname(outputPath);
  if (dir && dir !== ".") {
    fs.mkdirSync(dir, { recursive: true });
  }

  fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));

  console.log(`\nāœ“ Found ${results.length} matches out of ${items.length} items`);
  console.log(`  Query: "${query}"`);
  console.log(`  Fields: ${fields.join(", ")}`);
  console.log(`  Mode: ${mode}`);
  console.log(`  Written to: ${outputPath}`);

  console.log(
    JSON.stringify({
      success: true,
      outputPath,
      query,
      fields,
      mode,
      inputCount: items.length,
      matchCount: results.length,
    })
  );
} catch (error) {
  console.error("Error:", error.message);
  process.exit(1);
}