Skip to content

Instantly share code, notes, and snippets.

@jmakeig
Last active May 24, 2017 23:01
Show Gist options
  • Save jmakeig/fd1789ff731955dfd42237d7f35737aa to your computer and use it in GitHub Desktop.
Save jmakeig/fd1789ff731955dfd42237d7f35737aa to your computer and use it in GitHub Desktop.
Search MarkLogic Query Console buffers

Searches all Query Console workspaces. Uses exact match, like you’d do in the browser’s own find functionality. Only matches current buffers, not histories.

Set-up

  1. Enable word search indexes in the App-Services database
  2. Paste qconsole-search.sjs above into a Query Console query buffer (how meta)
  3. Modify the first parameter of the findBuffers() call at bottom to change the actual search term

To-do

  1. Weight highest to lowest: current query buffer, active buffers, current workspace, other workspaces
  2. Figure out security: There don’t look to be document permissions (other than qconsole-internal) on any of the Query Console data. (Shouldn’t workspaces, queries, and histories be “owned” by a user?)
'use strict';
/**
* Look up the name and query mode of a query by its unique ID.
*
* @param {string} - the unique identifier of a query buffer
* @return {object|undefined} - an object with `name` and `mode` keys
or `undefined` if no query matches the ID
*/
function lookupQuery(queryID) {
const db = require('/MarkLogic/jsearch');
const docs = db.documents();
const ns = { qc: 'http://marklogic.com/appservices/qconsole' };
const query = cts.andQuery([
cts.directoryQuery('/workspaces/'),
//cts.elementQuery(fn.QName(ns.qc, 'query'),
cts.elementValueQuery(fn.QName(ns.qc, 'id'), queryID, ['exact'])
//)
]);
const results = docs.where(query).filter(true).result().results;
if (results && results.length) {
const doc = results[0].document;
const queryEl = fn.head(doc.xpath(`//qc:query[qc:id = '${queryID}']`, ns));
return {
name: queryEl.xpath(`./qc:name/string(.)`, ns),
mode: queryEl.xpath(`./qc:mode/string(.)`, ns),
workspace: {
name: doc.xpath(`/qc:workspace/qc:name/string(.)`, ns),
id: doc.xpath(`/qc:workspace/qc:id/string(.)`, ns)
}
};
}
//throw new Error(String(queryID));
}
/**
* Highlight exact matches of a phrase in a query buffer.
*
* @param {string} buffer - the entire buffer of code
* @param {string} phrase - the exact text to match
* @param {number} [num=3] - the number of lines before an after the match to inlcude
* @yield {object} - `location` object with `line` and `column` properties
and `context` array with the matched lines and surrounding context
*/
function* highlight(buffer, phrase, num = 3, maxMatches = 50) {
if (num < 1) {
throw new TypeError('num must be greater than zero');
}
num = Math.floor(num);
if (maxMatches < 1) {
throw new TypeError('maxMatches must be greater than zero');
}
const re = new RegExp(phrase, 'g');
//console.dir(re);
const lineNumbers = (lines, start) =>
lines.map(line => ({ number: start++, text: line }));
const lines = buffer.split('\n'); // TODO: Pass in a Iterable<string> of lines
const lower = x => Math.max(x, 0);
const upper = x => Math.min(x, lines.length - 1);
let i = 0;
for (const line of lines) {
if (re.test(line)) {
console.log(line);
yield {
location: { line: i + 1, column: line.indexOf(phrase) + 1 },
context: lineNumbers(
[].concat(
lines.slice(lower(i - num), i), // context before
lines[i].replace(re, `<highlight>${phrase}</highlight>`), // TODO: Is there a better way to do this?
lines.slice(upper(i + 1), upper(i + 1 + num)) // context after
),
lower(i - num) + 1
)
};
if (i >= maxMatches - 2) {
//break;
}
}
i++;
}
}
/**
* Emit search hits on Query Console queries using an
* exact match on the `queryString`.
*
* @param {string} queryString - the exact phrase to search for
* @yield {object} - a document and its match context
*/
function* findBuffers(queryString) {
const db = require('/MarkLogic/jsearch');
const docs = db.documents();
const query = cts.andQuery([
cts.directoryQuery('/queries/'),
cts.wordQuery(queryString, ['exact'])
]);
const results = docs.where(query).filter(true).slice(0, 10).result('iterator')
.results;
for (const hit of results) {
const uri = hit.uri /* xs.anyURI */.toString();
const qID = uri.split('/')[2].split('.')[0];
yield Object.assign(lookupQuery(qID) || {}, {
uri: uri,
matches: Array.from(highlight(hit.document.toObject(), queryString))
});
}
}
Array.from(findBuffers('Error.captureStackTrace'));
[
{
"name": "Query 1",
"mode": "javascript",
"workspace": {
"name": "Workspace",
"id": "14039582673742374135"
},
"uri": "/queries/3692872500376590321.txt",
"matches": [
{
"location": {
"line": 11,
"column": 5
},
"context": [
{
"number": 8,
"text": " super();"
},
{
"number": 9,
"text": " this.uri = uri;"
},
{
"number": 10,
"text": " this.message = `URI ${String(uri)} exists already. Inserts can only write to new URIs.`;"
},
{
"number": 11,
"text": " <highlight>Error.captureStackTrace</highlight>(this);"
},
{
"number": 12,
"text": " }"
},
{
"number": 13,
"text": " get name() {"
},
{
"number": 14,
"text": " return 'DocumentExistsError';"
}
]
},
{
"location": {
"line": 26,
"column": 5
},
"context": [
{
"number": 23,
"text": " super();"
},
{
"number": 24,
"text": " this.uri = uri;"
},
{
"number": 25,
"text": " this.message = `URI ${String(uri)} does not exist already. Updates must happen to an existing URI.`;"
},
{
"number": 26,
"text": " <highlight>Error.captureStackTrace</highlight>(this);"
},
{
"number": 27,
"text": " }"
},
{
"number": 28,
"text": " get name() {"
},
{
"number": 29,
"text": " return 'DocumentNotExistsError';"
}
]
}
]
},
{
"name": "workspace search",
"mode": "javascript",
"workspace": {
"name": "Workspace",
"id": "14039582673742374135"
},
"uri": "/queries/3159150439601961495.txt",
"matches": [
{
"location": {
"line": 121,
"column": 25
},
"context": [
{
"number": 118,
"text": " }"
},
{
"number": 119,
"text": "}"
},
{
"number": 120,
"text": ""
},
{
"number": 121,
"text": "Array.from(findBuffers('<highlight>Error.captureStackTrace</highlight>'));"
}
]
}
]
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment