I am new to RavenDB. I am trying to use a multi map index feature, though I am not sure if it is the best approach to my problem. So I have three documents: Unit, Car, People.
Car document looks like this:
{
Id: "cars/123",
PersonId: "people/1235",
UnitId: "units/4321",
Make: "Toyota",
Model: "Prius"
}
People document looks like this:
{
Id: "people/1235",
FirstName: "test",
LastName: "test"
}
And unit doc:
{
Id: "units/4321",
Address: "blah blah"
}
This is an abbreviated example, in my real app there are way more fields, so data de-normalization would be my last resort.
I need to create and index that will have all of this three docuemnts joined in one document. Something like this:
{
CarId: "cars/123",
PersonId: "people/1235",
UnitId: "units/4321",
Make: "Toyota",
Model: "Prius"
FirstName: "test",
LastName: "test"
Address: "blah blah"
}
// same unit different person owns a different car
{
CarId: "cars/122",
PersonId: "people/1236",
UnitId: "units/4321",
Make: "Toyota",
Model: "4runner"
FirstName: "test",
LastName: "test"
Address: "blah blah"
}
In a relational database I would just use two joins to People and Unit tables by ids
and my car table would be an aggregate entity.
Here is the index definition that I have:
public class MyMultiIndex : AbstractMultiMapIndexCreationTask<JoinedDocument>
{
public MyMultiIndex()
{
// creating maps
AddMap<Car>(cars => cars.Select(e => new { e.CarId, e.Make, e.Model, PersonId = e.PersonId, UnitId = e.UnitId, FirstName = (null)string, LastName = (null)string, Address = (nul)string }));
AddMap<People>(people => people.Select(e => new { CarId = (string)null, Make = (string)null, Model = (string)null, PersonId = e.Id, UnitId = (null)string, FirstName = e.FirstName, LastName = e.LastName, Address = (nul)string }));
AddMap<Unit>(people => people.Select(e => new { CarId = (string)null, Make = (string)null, Model = (string)null, PersonId = (null)string, UnitId = e.null, FirstName = (nul)string , LastName = (nul)string , Address = e.Address }));
Reduce = results => from result in results
group result by result.CarId
into g
select new JoinedDocument
{
CarId = g.Key,
PersonId = g.First(e => e.CarId == g.Key).PersonId,
UnitId = g.First(e => e.CarId == g.Key).UnitId,
Model = g.First(e => e.CarId == g.Key).Model,
Make = g.First(e => e.CarId == g.Key).Make,
**// this never works. It is like result set does not contain anything with this personId. It looks like AddMap for people document did not work.**
FirstName = results.First(e => e.PersonId == g.First(ie => ie.CarId == g.Key).PersonId).FirstName,
**// this never works. It is like result set does not contain anything with this personId. It looks like AddMap for people document did not work.**
LastName = results.First(e => e.PersonId == g.First(ie => ie.CarId == g.Key).PersonId).LastName,
**// this never works. It is like result set does not contain anything with this personId. It looks like AddMap for unit document did not work.**
UnitAddress = results.First(e => e.UnitId == g.First(ie => ie.CarId == g.Key).UnitId).LastName,
};
Index(map => map.Model, FieldIndexing.Analyzed);
Index(map => map.Make, FieldIndexing.Analyzed);
Index(map => map.LastName, FieldIndexing.Analyzed);
Index(map => map.FirstName, FieldIndexing.Analyzed);
Index(map => map.Make, FieldIndexing.Analyzed);
Index(map => map.UnitAddress, FieldIndexing.Analyzed);
}
}
When RavenDb runs this index I see errors when it is trying to run the Reduce function I have provided. It throws error when I am trying to match a record where person's first name and last name exist, same happens with the unit.
It looks like you're trying to fit a document database with an object model that has relationships. This blog may help you:
Keeping a Domain Model Pure with RavenDB
Keep in mind that this isn't the recommended use of RavenDB, but sometimes it's necessary, and this is a good way to handle it.
Can you have a car with no owner? or an address with no resident? If its false in both cases, I would model the car and unit to be embedded inside a person. So the person becomes your aggregate root and to get to a car or a unit you must go through a person.
Related
I have 3 entities.
VehicleType has a many vehicles and each vehicle has a many rents. Each rent has TotalKm.
I want to know the sum of TotalKm for all rents for each vehicle type.
I try this:
public async Task<IEnumerable<VehicleTypeDto>> GetAllAsync()
{
return await _unitOfWork.
VehicleTypesRepository.
GetEntityAsNoTracking().
Include(entity => entity.Vehicles).ThenInclude(entity => entity.Rents).ThenInclude(entity => entity.Damages).
Select(entity => new VehicleTypeDto
{
Id = entity.Id,
Code = entity.Code,
Description = entity.Description,
TotalKm = entity.Vehicles.Sum(v => v.Rents.Sum(r => r.TotalKm )) ?? 0,
}).
OrderBy(entity => entity.Description).
ToListAsync();
}
But I have this error:
Cannot perform an aggregate function on an expression containing an
aggregate or a subquery.
Someone can help me pls?
I have the following tables:
users:
------
id
first_name
current_property_id
property_users:
---------------
id
user_id
property_id
chat_user_id
nickname
chat_users:
-----------
id
identity
chat_groups:
------------
id
name
type ['Single' means the group a user is having permutation without repetition for all property users that will serve as their 1:1 conversation (in short everyone is paired with everyone once), 'Group' means more than 2 people can be in the group]
last_messaged_at
chat_group_members:
-------------------
id
chat_group_id
user_id
chat_user_id
I need to make a query to fetch all users (except me) in the property I am in and get the chat_groups I have with them (Type == Single).
I tried doing it PHP way but it takes too long (for 125 users it takes +30s of response)
This is how I am doing it in PHP
$this->currentUser = auth()->user();
//this is Fluent using DB::table and joining property_users, chat_users, and users
$propertyUsers = $this->run(GetPropertyUsersJob::class, [
'property' => $this->property
]);
//this is using simple eloquent getting groups
$chatGroups = $this->run(GetGroupsJob::class, [
'property' => $this->property,
'type' => ChatGroup::TYPE_SINGLE
]);
$chatGroups->load('members.user');
//first level of loop
foreach($propertyUsers as $propertyUser) {
if($this->currentUser->id == $propertyUser->user_id) {
return [];
}
$chatGroupId = '';
$chatGroupName = '';
//second level of loop
foreach($chatGroups as $chatGroup) {
$found = 0;
//third level of loop
foreach($chatGroup->members as $member) {
if($member->user_id == $this->currentUser->id) {
$found++;
}
if($member->user_id == $propertyUser->user_id) {
$found++;
}
}
//just to get this, I need to do nested loop 3x
if($found == 2) {
$chatGroupId = $chatGroup->id;
$chatGroupName = $chatGroup->name;
}
}
$user[] = [
'id' => $propertyUser->user_id,
'first_name' => $propertyUser->user->first_name,
'nickname' => $propertyUser->nickname,
'chat_group_id' => $chatGroupId,
'chat_group_name' => $chatGroupName
];
}
How can I convert this to a query (eloquent/fluent) instead?
#Update:
User model: I am only adding related eager loading
-----------
public function property_users()
{
return $this->hasMany(PropertyUser::class, 'user_id', 'id');
}
PropertyUser model:
-------------------
public function user()
{
return $this->belongsTo(User::class);
}
public function chat_user()
{
return $this->hasOne(ChatUser::class, 'id', 'chat_user_id');
}
ChaUser model:
--------------
public function property_user()
{
return $this->belongsTo(PropertyUser::class, 'id', 'chat_user_id');
}
my code is it.. using nest, elasticsearch 2.3.0 Version
i want mapping( + custom analyzer) & create index ...
but mapping is not unsuccessful low level call error!
please , check my code and review for me
var node = new Uri("http://localhost:9200");
var settings = new ConnectionSettings(node);
var client = new ElasticClient(settings);
var request = new IndexExistsRequest("aa");
var result = client.IndexExists(request);
if (result.Exists == true)
{
client.DeleteIndex("aa", null);
}
var ilhee_Custom = new CustomAnalyzer
{
Filter = new List<string> { "lowercase", "stop", "standard", "snowball" },
Tokenizer = "standard"
};
List<Person> categList = new List<Person>();
var Person = new Person
{
id = 1,
Firstname = "an apples bananas boxes, the sun.",
Lastname = "a beautiful womens with a good guys in there"
};
categList.Add(Person);
var response = client.CreateIndex("aa");
var mappingResponse = client.Map<Person>(d => d
.Properties(props => props
.String(s => s
.Name(p => p.Firstname)
.Index(FieldIndexOption.Analyzed)
.Analyzer("ilhee_Custom")
)
.String(s1 => s1
.Name(p1 => p1.Lastname)
.NotAnalyzed()
)
)
.Index("aa")
.Type("person")
);
var b = client.IndexMany<Person>(categList, "aa", "person");
You create a custom analyzer but you don't send it to Elasticsearch, so when it comes to using it in a mapping, Elasticsearch knows nothing about the custom analyzer.
You can create a new index with analysis and mappings in one request. Here's an example of creating an index, adding your custom analyzer and mapping as part of the index creation
void Main()
{
var node = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var settings = new ConnectionSettings(node)
// set "aa" as the default index; if no index
// is specified for a type or in the request,
// the default index will be used
.DefaultIndex("aa");
var client = new ElasticClient(settings);
var indexExistsResponse = client.IndexExists("aa");
if (indexExistsResponse.Exists)
{
client.DeleteIndex("aa", null);
}
var people = new List<Person>{
new Person
{
id = 1,
Firstname = "an apples bananas boxes, the sun.",
Lastname = "a beautiful womens with a good guys in there"
}
};
var createIndexResponse = client.CreateIndex("aa", c => c
.Settings(s => s
.Analysis(a => a
.Analyzers(ad => ad
// give the custom analyzer a name
.Custom("ilhee_Custom", ca => ca
.Tokenizer("standard")
.Filters("lowercase", "stop", "standard", "snowball")
)
)
)
)
.Mappings(m => m
.Map<Person>(d => d
.Properties(props => props
.String(s => s
.Name(p => p.Firstname)
.Analyzer("ilhee_Custom")
)
.String(s1 => s1
.Name(p1 => p1.Lastname)
.NotAnalyzed()
)
)
)
)
);
var indexManyResponse = client.IndexMany<Person>(people, "aa");
// refresh the index after indexing, so that newly indexed documents
// are available in search results.
client.Refresh("aa");
var searchResponse = client.Search<Person>(s => s
.Query(q => q
.Match(m => m
.Field(p => p.Firstname)
.Query("boxes")
)
)
);
}
public class Person
{
public int id { get; set; }
public string Firstname { get; set; }
public string Lastname { get; set;}
}
The search returns back our indexed document as expected
{
"took" : 9,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 0.15342641,
"hits" : [ {
"_index" : "aa",
"_type" : "person",
"_id" : "1",
"_score" : 0.15342641,
"_source" : {
"id" : 1,
"firstname" : "an apples bananas boxes, the sun.",
"lastname" : "a beautiful womens with a good guys in there"
}
} ]
}
}
As shown in the following unit test I expect stats.TotalResults to be 2 but is 3. Why is that?
[Test]
public void RavenQueryStatisticsTotalResultsTest1()
{
using (var db = _documentStore.OpenSession())
{
db.Store(new Club { Name = "Foo1", Type = "Amateur" }); // --> Matches all conditions
db.Store(new Club { Name = "Foo2", Type = "Professional" });
db.Store(new Club { Name = "Foo3", Type = "Amateur" }); // --> Matches all conditions
db.Store(new Club { Name = "Boo1", Type = "Amateur" });
db.Store(new Club { Name = "Boo2", Type = "Professional" });
db.SaveChanges();
}
WaitForIndexing(_documentStore);
using (var db = _documentStore.OpenSession())
{
RavenQueryStatistics stats;
var query = db.Query<Club>()
.Statistics(out stats)
.Where(club => club.Type == "Amateur")
.Intersect()
.Search(club => club.Name, "Foo*", escapeQueryOptions: EscapeQueryOptions.AllowPostfixWildcard);
var clubs = query.ToList();
Assert.AreEqual(2, clubs.Count);
Assert.AreEqual(2, stats.TotalResults); // I expect 2 but was 3! Why is that?
}
}
You need Intersect when using multiple where clauses. To combine Where and Search, you have to pass SearchOptions.And to Search:
using (var db = _documentStore.OpenSession()) {
RavenQueryStatistics stats;
var query = db.Query<Club>()
.Customize(_ => _.WaitForNonStaleResultsAsOfLastWrite())
.Statistics(out stats)
.Where(club => club.Type == "Amateur")
.Search(
club => club.Name,
"Foo*",
escapeQueryOptions: EscapeQueryOptions.AllowPostfixWildcard,
options:SearchOptions.And);
var clubs = query.ToList();
Assert.AreEqual(2, clubs.Count);
Assert.AreEqual(2, stats.TotalResults);
}
I have a question about formatting the friendly URL for category and subcategory and getting the matching products. I am using PrestaShop 1.5.2.0
Let's say we have a structure like this:
Category 1
Spare Parts
Accessories
Category 2
Chips
Accessories
I want to display the link like this: /category-1/accessories and to display the products from category 1->accessories. How can I achieve this?
The current behavior is when I click on accessories, being in category 1, the link is /accessories and the products that are displayed belong from both /category-1/accessories and /category-2/accessories
Thanks!
This question was answered on the PrestaShop forum.
You can find it here http://www.prestashop.com/forums/topic/220017-category-subcategory-url/
The solution - add this changes to the fallowing classes
CLASSES/Dispatcher.php
'rule' => '{categories:/}{id}-{rewrite}/',
'categories' => array('regexp' => '[/_a-zA-Z0-9-\pL]*'),
CLASSES/Link.php
$cats = array();
foreach ($category->getParentsCategories() as $cat)
if (!in_array($cat['id_category'], array(1, 2, $category->id)))//remove root, home and current category from the URL
$cats[] = $cat['link_rewrite'];
$params['categories'] = implode('/', array_reverse($cats));
The above code is valid for the older versions, but it would not work for the newer/latest version. I updated the same solution for the newer version (1.7.7.4.), it might be used for others.
change CLASSES/Dispatcher.php
at about line 55 in the above file copy paste
public $default_routes = [
'category_rule' => [
'controller' => 'category',
**/** added 1 line below*/
'rule' => '{category:/}{id}-{rewrite}/',
/** commented 1line below*/
/**'rule' => '{id}-{rewrite}',*/
'keywords' => [
'id' => ['regexp' => '[0-9]+', 'param' => 'id_category'],
/*** added 1 line below*/
'category' => ['regexp' => '[_a-zA-Z0-9-\pL]*'],
'rewrite' => ['regexp' => self::REWRITE_PATTERN],
'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'],
'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'],**
],
],
In the file classes/link.php find the function getCategoryLink and replace it with the function code below
/**
* Create a link to a category.
*
* #param mixed $category Category object (can be an ID category, but deprecated)
* #param string $alias
* #param int $idLang
* #param string $selectedFilters Url parameter to autocheck filters of the module blocklayered
*
* #return string
*/
public function getCategoryLink(
$category,
$alias = null,
$idLang = null,
$selectedFilters = null,
$idShop = null,
$relativeProtocol = false
) {
$dispatcher = Dispatcher::getInstance();
if (!$idLang) {
$idLang = Context::getContext()->language->id;
}
$url = $this->getBaseLink($idShop, null, $relativeProtocol) . $this->getLangLink($idLang, null, $idShop);
// Set available keywords
$params = [];
if (!is_object($category)) {
$params['id'] = $category;
} else {
$params['id'] = $category->id;
}
// Selected filters is used by the module ps_facetedsearch
$selectedFilters = null === $selectedFilters ? '' : $selectedFilters;
if (empty($selectedFilters)) {
$rule = 'category_rule';
} else {
$rule = 'layered_rule';
$params['selected_filters'] = $selectedFilters;
}
if (!$alias) {
$category = $this->getCategoryObject($category, $idLang);
}
$params['rewrite'] = (!$alias) ? $category->link_rewrite : $alias;
if ($dispatcher->hasKeyword($rule, $idLang, 'meta_keywords', $idShop)) {
$category = $this->getCategoryObject($category, $idLang);
$params['meta_keywords'] = Tools::str2url($category->getFieldByLang('meta_keywords'));
}
if ($dispatcher->hasKeyword($rule, $idLang, 'meta_title', $idShop)) {
$category = $this->getCategoryObject($category, $idLang);
$params['meta_title'] = Tools::str2url($category->getFieldByLang('meta_title'));
}
if ($category !='var'){
$category = $this->getCategoryObject($category, $idLang);
$pcategory= new Category($category->id_parent, $idLang);
if($category->id_parent!=1 && $category->id_parent!=2){
$params['category'] = $pcategory->link_rewrite;
//append the categoryID with its name
$params['category'] = $category->id_parent . '-'. $params['category'];
}
}
return $url . Dispatcher::getInstance()->createUrl($rule, $idLang, $params, $this->allow, '', $idShop);
}
in the same file classes/link.php update if condition as follows at line 218 in the code for (function getProductLink)
if ($dispatcher->hasKeyword('product_rule', $idLang, 'categories', $idShop)) {
$product = $this->getProductObject($product, $idLang, $idShop);
$params['category'] = (!$category) ? $product->category : $category;
$cats = [];
foreach ($product->getParentCategories($idLang) as $cat) {
if (!in_array($cat['id_category'], Link::$category_disable_rewrite)) {
//remove root and home category from the URL
//commented the line below
//$cats[] = $cat['link_rewrite'];
//replaced the above line with the line below to append the category ID in the products link
$cats[] = $cat['id_category'].'-'.$cat['link_rewrite'];
}
}
$params['categories'] = implode('/', $cats);
}
I happened to be using the prestashop version 1.7.7.4. You can see this solution working on my site https://jinbaba.pk
Also after making the above changes in the code files, do not forget to update shopparameters-->SEO&URL Setting, change the category and product routes as follows (if they are not already like this)
"Route to category" = {category:/}{id}-{rewrite}
"Route to product" = {categories:/}{id}{-:id_product_attribute}-{rewrite}{-:ean13}.html
Just a suggestion for SEO : You do not need to remove category ID and product ID from the URL. They have minimal or no impact on SEO.
also the about solution will work for 2 level nesting e.g.
yourdomain.com/category-1/category-2/1-product.html
do not create more nesting of categories in your catalog. If you plan to do create deeper nesting your site you will need to update this solution. However, for SEO deep nesting is not recommended.