1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Enhancements to search results, and event context implementation

This change adds support to the JDK for processing the results of a room
search, as well as back-paginating the results.

It treats each search result as a 'context' object, which can itself be
backwards or forward-paginated.
This commit is contained in:
Richard van der Hoff
2016-01-04 12:50:07 +00:00
committed by review.rocks
parent 3e4cef89fd
commit c669d21af7
3 changed files with 339 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ var q = require("q");
var httpApi = require("./http-api"); var httpApi = require("./http-api");
var MatrixEvent = require("./models/event").MatrixEvent; var MatrixEvent = require("./models/event").MatrixEvent;
var EventStatus = require("./models/event").EventStatus; var EventStatus = require("./models/event").EventStatus;
var SearchResult = require("./models/search-result");
var StubStore = require("./store/stub"); var StubStore = require("./store/stub");
var webRtcCall = require("./webrtc/call"); var webRtcCall = require("./webrtc/call");
var utils = require("./utils"); var utils = require("./utils");
@@ -1901,6 +1902,73 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
return defer.promise; return defer.promise;
}; };
/**
* Take an EventContext, and back/forward-fill results.
*
* @param {module:models/event-context.EventContext} eventContext context
* object to be updated
* @param {Object} opts
* @param {boolean} opts.backwards true to fill backwards, false to go forwards
* @param {boolean} opts.limit number of events to request
*
* @return {module:client.Promise} Resolves: updated EventContext object
* @return {Error} Rejects: with an error response.
*/
MatrixClient.prototype.paginateEventContext = function(eventContext, opts) {
// TODO: we should implement a backoff (as per scrollback()) to deal more
// nicely with HTTP errors.
opts = opts || {};
var backwards = opts.backwards || false;
var token = eventContext.getPaginateToken(backwards);
if (!token) {
// no more results.
return q.reject(new Error("No paginate token"));
}
var dir = backwards ? 'b' : 'f';
var pendingRequest = eventContext._paginateRequests[dir];
if (pendingRequest) {
// already a request in progress - return the existing promise
return pendingRequest;
}
var path = utils.encodeUri(
"/rooms/$roomId/messages", {$roomId: eventContext.getEvent().getRoomId()}
);
var params = {
from: token,
limit: ('limit' in opts) ? opts.limit : 30,
dir: dir
};
var self = this;
var promise =
self._http.authedRequest(undefined, "GET", path, params
).then(function(res) {
var token = res.end;
if (res.chunk.length === 0) {
token = null;
} else {
var matrixEvents = utils.map(res.chunk, self.getEventMapper());
if (backwards) {
// eventContext expects the events in timeline order, but
// back-pagination returns them in reverse order.
matrixEvents.reverse();
}
eventContext.addEvents(matrixEvents, backwards);
}
eventContext.setPaginateToken(token, backwards);
return eventContext;
}).finally(function() {
eventContext._paginateRequests[dir] = null;
});
eventContext._paginateRequests[dir] = promise;
return promise;
};
// Registration/Login operations // Registration/Login operations
// ============================= // =============================
@@ -2095,6 +2163,122 @@ MatrixClient.prototype.searchMessageText = function(opts, callback) {
}, callback); }, callback);
}; };
/**
* Perform a server-side search for room events.
*
* The returned promise resolves to an object containing the fields:
*
* * {number} count: estimate of the number of results
* * {string} next_batch: token for back-pagination; if undefined, there are
* no more results
* * {Array} highlights: a list of words to highlight from the stemming
* algorithm
* * {Array} results: a list of results
*
* Each entry in the results list is a {module:models/search-result.SearchResult}.
*
* @param {Object} opts
* @param {string} opts.term the term to search for
* @param {Object} opts.filter a JSON filter object to pass in the request
* @return {module:client.Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.searchRoomEvents = function(opts) {
// TODO: support groups
var body = {
search_categories: {
room_events: {
search_term: opts.term,
filter: opts.filter,
order_by: "recent",
event_context: {
before_limit: 1,
after_limit: 1,
include_profile: true,
}
}
}
};
var searchResults = {
_query: body,
results: [],
highlights: [],
};
return this.search({body: body}).then(
this._processRoomEventsSearch.bind(this, searchResults)
);
};
/**
* Take a result from an earlier searchRoomEvents call, and backfill results.
*
* @param {object} searchResults the results object to be updated
* @return {module:client.Promise} Resolves: updated result object
* @return {Error} Rejects: with an error response.
*/
MatrixClient.prototype.backPaginateRoomEventsSearch = function(searchResults) {
// TODO: we should implement a backoff (as per scrollback()) to deal more
// nicely with HTTP errors.
if (!searchResults.next_batch) {
return q.reject(new Error("Cannot backpaginate event search any further"));
}
if (searchResults.pendingRequest) {
// already a request in progress - return the existing promise
return searchResults.pendingRequest;
}
var searchOpts = {
body: searchResults._query,
next_batch: searchResults.next_batch,
};
var promise = this.search(searchOpts).then(
this._processRoomEventsSearch.bind(this, searchResults)
).finally(function() {
searchResults.pendingRequest = null;
});
searchResults.pendingRequest = promise;
return promise;
};
/**
* helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the
* response from the API call and updates the searchResults
*
* @param {Object} searchResults
* @param {Object} response
* @return {Object} searchResults
* @private
*/
MatrixClient.prototype._processRoomEventsSearch = function(searchResults, response) {
var room_events = response.search_categories.room_events;
searchResults.count = room_events.count;
searchResults.next_batch = room_events.next_batch;
// combine the highlight list with our existing list; build an object
// to avoid O(N^2) fail
var highlights = {};
room_events.highlights.forEach(function(hl) { highlights[hl] = 1; });
searchResults.highlights.forEach(function(hl) { highlights[hl] = 1; });
// turn it back into a list.
searchResults.highlights = Object.keys(highlights);
// append the new results to our existing results
for (var i = 0; i < room_events.results.length; i++) {
var sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper());
searchResults.results.push(sr);
}
return searchResults;
};
/** /**
* Perform a server-side search. * Perform a server-side search.
* @param {Object} opts * @param {Object} opts

104
lib/models/event-context.js Normal file
View File

@@ -0,0 +1,104 @@
"use strict";
/**
* @module models/event-context
*/
/**
* Construct a new EventContext
*
* An eventcontext is used for circumstances such as search results, when we
* have a particular event of interest, and a bunch of events before and after
* it.
*
* It also stores pagination tokens for going backwards and forwards in the
* timeline.
*
* @param {MatrixEvent} ourEvent the event at the centre of this context
*
* @constructor
*/
function EventContext(ourEvent) {
this._timeline = [ourEvent];
this._ourEventIndex = 0;
this._paginateTokens = {b: null, f: null};
// this is used by MatrixClient to keep track of active requests
this._paginateRequests = {b: null, f: null};
}
/**
* Get the main event of interest
*
* This is a convenience function for getTimeline()[getOurEventIndex()].
*
* @return {MatrixEvent} The event at the centre of this context.
*/
EventContext.prototype.getEvent = function() {
return this._timeline[this._ourEventIndex];
};
/**
* Get the list of events in this context
*
* @return {Array} An array of MatrixEvents
*/
EventContext.prototype.getTimeline = function() {
return this._timeline;
};
/**
* Get the index in the timeline of our event
*
* @return {Number}
*/
EventContext.prototype.getOurEventIndex = function() {
return this._ourEventIndex;
};
/**
* Get a pagination token.
*
* @param {boolean} backwards true to get the pagination token for going
* backwards in time
* @return {string}
*/
EventContext.prototype.getPaginateToken = function(backwards) {
return this._paginateTokens[backwards ? 'b' : 'f'];
};
/**
* Set a pagination token.
*
* Generally this will be used only by the matrix js sdk.
*
* @param {string} token pagination token
* @param {boolean} backwards true to set the pagination token for going
* backwards in time
*/
EventContext.prototype.setPaginateToken = function(token, backwards) {
this._paginateTokens[backwards ? 'b' : 'f'] = token;
};
/**
* Add more events to the timeline
*
* @param {Array} events new events, in timeline order
* @param {boolean} atStart true to insert new events at the start
*/
EventContext.prototype.addEvents = function(events, atStart) {
// TODO: should we share logic with Room.addEventsToTimeline?
// Should Room even use EventContext?
if (atStart) {
this._timeline = events.concat(this._timeline);
this._ourEventIndex += events.length;
} else {
this._timeline = this._timeline.concat(events);
}
};
/**
* The EventContext class
*/
module.exports = EventContext;

View File

@@ -0,0 +1,51 @@
"use strict";
/**
* @module models/search-result
*/
var EventContext = require("./event-context");
var utils = require("../utils");
/**
* Construct a new SearchResult
*
* @param {number} rank where this SearchResult ranks in the results
* @param {event-context.EventContext} eventContext the matching event and its
* context
*
* @constructor
*/
function SearchResult(rank, eventContext) {
this.rank = rank;
this.context = eventContext;
}
/**
* Create a SearchResponse from the response to /search
* @static
* @param {Object} jsonObj
* @param {function} eventMapper
* @return {SearchResult}
*/
SearchResult.fromJson = function(jsonObj, eventMapper) {
var jsonContext = jsonObj.context || {};
var events_before = jsonContext.events_before || [];
var events_after = jsonContext.events_after || [];
var context = new EventContext(eventMapper(jsonObj.result));
context.setPaginateToken(jsonContext.start, true);
context.addEvents(utils.map(events_before, eventMapper), true);
context.addEvents(utils.map(events_after, eventMapper), false);
context.setPaginateToken(jsonContext.end, false);
return new SearchResult(jsonObj.rank, context);
};
/**
* The SearchResult class
*/
module.exports = SearchResult;