Skip to content

Commit

Permalink
Added group and facet methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Nataniel López committed Apr 5, 2022
1 parent 1c78a7f commit aa5074e
Show file tree
Hide file tree
Showing 7 changed files with 1,088 additions and 386 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- `group()` method for grouping results
- `facet()` method for getting facet counts

## [1.7.2] - 2020-11-27
### Fixed
Expand Down
110 changes: 110 additions & 0 deletions README.md
Expand Up @@ -260,6 +260,116 @@ Searches distinct values of a property in a solr core
- Resolves `Array<Object>`: An array of documents
- Rejects `SolrError`: When something bad occurs

### ***async*** `group(model, [parameters])`
Group results by field or fields

- model: `Model`: A model instance
- parameters: `Object` (required):
- **field** `String|Array<string>`: The field or fields to group by
- **order** `Object`: -- described below in `get()` method.
- **filters** `Object`: -- described below in `get()` method.
- **limit** `Number`: -- described below in `get()` method. (Default: 1)

- Resolves `Object`: An object with the grouped field and its count and items.
- Rejects `SolrError`: When something bad occurs

Example:
```js

const groupedResponse = await solr.group(model, { field: 'someField' });

// Expected response
{
someField: {
count: 150,
groups: {
someFieldValue: {
count: 75,
items: [
{ someField: 'someFieldValue', otherField: 'otherFieldValue' },
// ...
]
},
someFieldValue2: {
count: 75,
items: [
{ someField: 'someFieldValue2', otherField: 'anotherFieldValue' },
// ...
]
}
}
}
}
```

### ***async*** `facet(model, [parameters])`
Get facet counts by field or pivot

- model: `Model`: A model instance
- parameters: `Object` (required):
- **field** `String`: The facet field (required if pivot is not received)
- **pivot** `String|Array<string>`: The facet pivot (required if field is not received)
- This field gives different results depending of how you use it:
- Sending a string with the field name (`{ pivot: 'myField' }`) will facet the results by that field (same as using field param).
- Sending a string with more than a field separated by comma (`{ pivot: 'myField,anotherField' }`) will facet results by that field and the rest of the combination recursively.
- Sending an array of strings with field names (`['myField', 'anotherField']`) will facet the results by each of them but the response will still be a single array.
- **order** `Object`: -- described below in `get()` method.
- **filters** `Object`: -- described below in `get()` method.
- **limit** `Number`: -- described below in `get()` method. (Default: 1)
- **page** `Number`: -- described below in `get()` method.

- Resolves `Array<object>`: An array with the facet results
- Rejects `SolrError`: When something bad occurs

Example:
```js

// Using field
const facetResponse = await solr.facet(model, { field: 'someField' });

// Expected response
[
{ field: 'someField', value: 'someFieldValue', count: 125 },
{ field: 'someField', value: 'otherFieldValue', count: 356 }
]

// Using pivot (single field)
const facetResponse = await solr.facet(model, { pivot: 'someField' });

// Expected response
[
{ field: 'someField', value: 'someFieldValue', count: 125 },
{ field: 'someField', value: 'otherFieldValue', count: 356 }
]

// Using pivot (combined fields)
const facetResponse = await solr.facet(model, { pivot: 'someField,otherField' });

// Expected response
[
{
field: 'someField',
value: 'someFieldValue',
count: 150,
pivot: [
{ field: 'otherField', value: 'otherFieldValue', count: 75 },
{ field: 'otherField', value: 'otherFieldValue2', count: 75 }
]
}
]

// Using pivot (array of fields)
const facetResponse = await solr.facet(model, { pivot: ['someField', 'otherField'] });

// Expected response
[
{ field: 'someField', value: 'someFieldValue', count: 125 },
{ field: 'someField', value: 'otherFieldValue', count: 356 },
{ field: 'otherField', value: 'otherFieldValue', count: 3025 },
{ field: 'otherField', value: 'anotherFieldValue', count: 1250 }
]
```

### ***async*** `get(model, [parameters])`
Searches documents in a solr core

Expand Down
54 changes: 54 additions & 0 deletions lib/helpers/query.js
Expand Up @@ -57,6 +57,60 @@ class Query {
};
}

static facet(params) {

const {
field, pivot, fields, limit, page
} = params;

if(!field && !pivot)
throw new SolrError('At least a facet field or pivot should be received.', SolrError.codes.INVALID_PARAMETERS);

if(field && typeof field !== 'string')
throw new SolrError(`Facet field must be a string, received: ${typeof field}.`, SolrError.codes.INVALID_PARAMETERS);

if(pivot && typeof pivot !== 'string' && !Array.isArray(pivot))
throw new SolrError(`Facet pivot must be a string or array, received: ${typeof pivot}.`, SolrError.codes.INVALID_PARAMETERS);

const filters = params.filters ? { filter: Filters.build(params.filters, fields) } : {};
const order = params.order ? { sort: this._getSorting(params.order) } : {};

return {
query: '*:*',
offset: (page * limit) - limit,
limit,
params: {
facet: true,
...field && { 'facet.field': field },
...pivot && { 'facet.pivot': pivot }
},
...filters,
...order
};
}

static group(params) {

const { field, fields, limit } = params;

if(typeof field !== 'string' && !Array.isArray(field))
throw new SolrError(`Group field must be a string or array, received: ${typeof field}.`, SolrError.codes.INVALID_PARAMETERS);

const filters = params.filters ? { filter: Filters.build(params.filters, fields) } : {};
const order = params.order ? { sort: this._getSorting(params.order) } : {};

return {
query: '*:*',
params: {
group: true,
'group.field': field,
'group.limit': limit
},
...filters,
...order
};
}

static _getSorting(order) {

return Object.entries(order).reduce((sortings, [field, term]) => {
Expand Down
36 changes: 36 additions & 0 deletions lib/helpers/response.js
Expand Up @@ -38,6 +38,42 @@ class Response {
});
}

static formatFacetFields(facetFields) {

const [[field, counts]] = Object.entries(facetFields);

const formattedFields = [];

for(let index = 0; index < counts.length; index += 2) {

const value = counts[index];
const count = counts[index + 1];

formattedFields.push({ field, value, count });
}

return formattedFields;
}

static formatFacetPivot(facetPivot) {
return Object.values(facetPivot).flat();
}

static formatGroup(grouped) {

return Object.entries(grouped).reduce((formattedGroups, [field, { matches, groups }]) => {

formattedGroups[field] = { count: matches, groups: {} };

groups.forEach(({ groupValue, doclist }) => {
formattedGroups[field].groups[groupValue] = { count: doclist.numFound, items: this.format(doclist.docs) };
});

return formattedGroups;

}, {});
}

static validate(res, terms = {}) {

const responseStruct = struct.partial(
Expand Down
97 changes: 97 additions & 0 deletions lib/solr.js
Expand Up @@ -15,6 +15,7 @@ const Schema = require('./helpers/schema');
const Query = require('./helpers/query');

const DEFAULT_LIMIT = 500;
const DEFAULT_GROUP_LIMIT = 1;

class Solr {

Expand Down Expand Up @@ -439,6 +440,102 @@ class Solr {
return docs.map(({ [params.key]: value }) => value);
}

/**
* Get facet counts from Solr database
* @param {Model} model Model instance
* @param {Object} params parameters (get params such as filters, order, etc.)
* @param {String} params.field The facet field
* @param {String|Array<string>} params.pivot The facet pivot (field, combined fields or array of fields)
* @returns {Array<object>} Facet result (if facet field and pivot is provided, the results shown will be only pivot ones)
* @throws When something goes wrong
* @example
* await facet(model, {
* field: 'myField',
* pivot: 'myField,anotherField'
* });
* // Expected result
* [
* {
* field: 'myField',
* value: 'someFieldValue',
* count: 1532,
* pivot: [
* { field: 'anotherField', value: 'anotherFieldValue', count: 512 },
* // ...
* ]
* },
* // ...
* ]
*/
async facet(model, params = {}) {

this._validateModel(model);

const { fields } = model.constructor;

const endpoint = Endpoint.create(Endpoint.presets.get, this.url, this.core);

const page = params.page || 1;
const limit = params.limit || DEFAULT_GROUP_LIMIT;

const query = Query.facet({ ...params, limit, page, fields });

const res = await this.request.get(endpoint, query);

Response.validate(res, {
facet_counts: {
...params.field && { facet_fields: 'object' },
...params.pivot && { facet_pivot: 'object' }
},
response: { docs: ['object'] }
});

const { facet_counts: { facet_fields: facetFields, facet_pivot: facetPivot } } = res;

return params.pivot ? Response.formatFacetPivot(facetPivot) : Response.formatFacetFields(facetFields);
}

/**
* Get grouped data from Solr database
* @param {Model} model Model instance
* @param {Object} params parameters (get params such as filters but sorting is not supported)
* @param {String|Array<string>} params.field The field or fields to group by
* @returns {Object} Grouped result
* @throws When something goes wrong
* @example
* await group(model, { field: 'myField' });
* // Expected result
* {
* myField: {
* count: 1234,
* groups: {
* myFieldValue: { count: 132, items: [...] },
* myFieldValue2: { count: gro234, items: [...] }
* }
* }
* }
*/
async group(model, params = {}) {

this._validateModel(model);

const { fields } = model.constructor;

const endpoint = Endpoint.create(Endpoint.presets.get, this.url, this.core);

const limit = params.limit || DEFAULT_GROUP_LIMIT;

const query = Query.group({ ...params, limit, fields });

const res = await this.request.get(endpoint, query);

Response.validate(res, { grouped: 'object' });

const { grouped } = res;

return Response.formatGroup(grouped);
}

/**
* Checks if the Solr host and core is online
* @returns {Boolean} true if the ping status is OK, false otherwise
Expand Down

0 comments on commit aa5074e

Please sign in to comment.