Firestore data structure for medium post - data-structures

I'd like to model a post structure like those in medium, with alternating paragraphs and photos.
[title]
[subTitle]
[main photo]
[paragraph 1]
[paragraph 2]
[photo 1]
[paragraph 3]
[photo 2]
[photo 3]
...
In order to maintain the order of paragraphs and photos, I am thinking of using array of object to model the post as below:
Posts (collection)
postId (document)
content: (Array)
title: string (Object)
subTitle: string (Object)
mainPhotoURL: string (Object)
paragraph: string (Object)
paragraph: string (Object)
photoURL: string (Object)
paragraph: string (Object)
...
Any comment on this thought? Any other data structure suggestion? Thanks.

Firestore document has a limit of 1MB. Arrays aren't scalable and addition/deletion is quite expensive and not yet available in all of admin SDKs. Every content should have it's own document.
Just like.
postId (document)
title: string (Object)
subTitle: string (Object)
mainPhotoURL: string (Object)
paragraph: string (Object)
paragraph: string (Object)
photoURL: string (Object)
paragraph: string (Object)
...

Related

TermsAggregation on Array

have a problem with an index field that stores a list of strings in the way of
["abc_def_fgh", "abc_def_fgh" ,"123_345_456"]
Im trying to use TermsAggregation to get
"abc_def_fgh" (2)
"123_345_456" (1)
But cant get it working, as it results the count for each of the terms (abc (2), def(2) , etc)
Any idea?
Many thanks
Try something like
var result = await client.SearchAsync<Document>(s => s
.Size(0)
.Aggregations(a => a
.Terms("tags", t => t.Field(f => f.Tags.Suffix("keyword")))));
foreach (var bucket in result.Aggregations.Terms("tags").Buckets)
{
System.Console.WriteLine($"Tag {bucket.Key}, doc count: {bucket.DocCount}");
}
public class Document
{
public string Id { get; set; }
public string[] Tags { get; set; } = { };
}
for these three documents in my index
new Document {Id = "1", Tags = new[]{"a","b"}
new Document {Id = "2", Tags = new[]{"a"}
new Document {Id = "3", Tags = new[]{"c"}
it will output
Tag a, doc count: 2
Tag b, doc count: 1
Tag c, doc count: 1
Hope that helps.
This sounds like a mapping issue. You need to make sure that you are using keyword opposed to text. You do not want the field to be analyzed when using aggregations.

YamlDotNet "Expected 'StreamEnd', got 'DocumentStart'"when reading string

I'm using YamlDotNet with an Azure Function v2 to serialise YAML from a markdown file (hosted on GitHub) to a .net object. I'm struggling with this error when attempting to deserialize the YAML string
Expected 'StreamEnd', got 'DocumentStart
I'm getting the markdown file content using HttpClient with a GET request to https://github.com/martinkearn/Content/raw/fd83bf8218b7c5e01f8b498e8a831bcd3fc3c961/Blogs/Test.md which returns a raw markdown file in the response body.
My Model is
public class Article
{
public string Title { get; set; }
public string Author { get; set; }
public List<string> Categories { get; set; }
}
My YAML is
---
title: Test File
author: Martin Kearn
categories:
- Test
- GitHubCMS
- One More Tag
- another tag
---
Here is my code
// get url from request body
var url = "https://raw.githubusercontent.com/martinkearn/Content/fd83bf8218b7c5e01f8b498e8a831bcd3fc3c961/Blogs/Test.md";
// get raw file and extract YAML
using (var client = new HttpClient())
{
//setup HttpClient
client.BaseAddress = new Uri(url);
client.DefaultRequestHeaders.Add("User-Agent", "ExtractYAML Function");
//setup httpContent object
var response = await client.GetAsync(url);
string rawFile = await response.Content.ReadAsStringAsync();
using (var r = new StringReader(rawFile))
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(new CamelCaseNamingConvention())
.Build();
//This line is causing Expected 'StreamEnd', got 'DocumentStart'
var article = deserializer.Deserialize<Article>(r);
}
}
Your actual downloaded file contains:
---
title: Test File
author: Martin Kearn
categories:
- Test
- GitHubCMS
- One More Tag
- another tag
---
# Test file
The --- is the end-of-directives marker, which is optional if you don't have any directives ( %YAML 1.2, %TAG .... ).
Since you have an empty line after the second directive, this is counted as if your second document contained
---
null
# Test file
You should at least get rid of that empty line and possible remove the second end-of-directives marker, putting the comment at the end of the first document
The end-of-document indicator in YAML is ... at the beginning of a line.
Make your file read:
title: Test File
author: Martin Kearn
categories:
- Test
- GitHubCMS
- One More Tag
- another tag
# Test file
or at most:
---
title: Test File
author: Martin Kearn
categories:
- Test
- GitHubCMS
- One More Tag
- another tag
# Test file

Handling IGetResponse on Nest

I am using the Get API of Nest, but I don't know how to typecast the respone (IGetResponse) to the specific type of the document, something like this:
var response = client.Get<MyDocument>(documentId);
return response.Document(); // Or something like this that returns a MyDocument type
Also, is there a way to get the document for another unique field or only the Id is accepted?
response.Source holds document of type MyDocument.
As documentation says, you can use get api to get documents only by their ids.
You can tell elasticsearch to treat other field from document as Id.
With NEST you can do this as follows:
var indicesOperationResponse = client.CreateIndex(descriptor => descriptor
.Index(indexName)
.AddMapping<Document>(m => m.IdField(f => f.Path("uniqueValue"))));
client.Index(new Document{UniqueValue = "value1"});
var getResponse = client.Get<Document>(g => g.Id("value1"));
My document class:
public class Document
{
public string UniqueValue { get; set; }
}

Construct views for data nested several levels deep

I'm trying to build an order system for a friend using django/tastypie on the server side and backbone/marionette on the client side. The server side poses no bigger problem but since I'm an inexperienced frontend developer I'm kinda stuck;
The simpler case went just fine, e.g. to list, add, edit and remove an Article (just a table in my database with sku, description and so on) using Composite- and ItemViews.The problem is when I'm trying to construct the views for an Order since it consists of several tables with relations on the server side.
Order
LineItem
Article
StoreQuantity
Store
StoreQuantity
Store
LineItem
Article
StoreQuantity
Store
StoreQuantity
Store
...
So an Order consists of several LineItems. A LineItem consists of an Article and several StoreQuantity:s making it possible to model something like "Order Article A; 10 copies to Store X and 4 copies to Store Y, Article B; 4 copies to Store X and 1 copy to Store Y".
I guess my question is; how would I go about to construct my views for something like above?
Would something like below be the wrong way?
Create an OrderCompositeView and pass it the OrderModel from my controller.
When OrderModel is fetched from the server, let OrderCompositeView create a LineItemCompositeView.
When LineItemCompositeView has fetched its' LineItemCollection from the server.. and so on recursively
Should I create a REST-url that returns the entire JSON for an Order and its relations instead of several smaller recursive calls, and then try to parse the JSON client side?
I've found several good resources on how to get going with Marionette but none on how to handle data nested several layers deep.
Thanks /Magnus
Edit:
Showing some code illustrating what I've been testing
(Views)
var LineItemDetailView = Backbone.Marionette.ItemView.extend({
template: "#lineitem-layout-template",
tagName: "div",
initialize: function() {
}
});
var LineItemView = Backbone.Marionette.CompositeView.extend({
template: "#lineitem-wrapper-template",
childView: LineItemDetailView,
childViewContainer: "div",
initialize: function(coll, obj) {
this.collection = new LineItemCollection({url: "api/v1/lineitem/?order__id=" + obj["order_id"]});
this.collection.fetch({
success: function() {
console.log("Successfully fetched lineitems");
}
});
}
});
var OrderDetailView = Backbone.Marionette.CompositeView.extend({
template: "#order-detail-template",
childView: LineItemView,
childViewContainer: "#lineitems",
initialize: function() {
this.model.on("sync", function(mod) {
lineitemView = new LineItemView([],{order_id: mod.get("id")});
});
}
});
Something along those lines. OrderDetailView is created from my controller and passed the OrderModel. I from this I get OrderDetailView:s template to render and the LineItemCollection is fetched from server but nothing more happens.
So I ran into this when creating a survey portion of an app the other day. It had a structure like this:
Survey:
Question:
Answer
Answer
Question:
Answer
Answer
So pretty similar to what you're doing. I used the backbone-relational gem - http://backbonerelational.org/ to relate the models together and it worked great. My API sends back all of the JSON in a single call. So surveys/1.json brings back all of the above pieces/their data. Then I parse/break them up with Backbone relational. Here's what they look like:
Survey:
class Entities.Survey extends App.Entities.Model
urlRoot: "surveys"
defaults:
status: "Draft"
number_taken: 0
survey_limit: 500
relations: [
type: Backbone.HasMany
key: "questions"
relatedModel: Entities.Question
reverseRelation:
key: 'survey'
includeInJSON: 'id'
]
Question:
class Entities.Question extends App.Entities.Model
urlRoot: "questions"
defaults:
single_response: true
terminate: false
free_text: false
relations: [
type: Backbone.HasMany
key: "answers"
relatedModel: Entities.Answer
reverseRelation:
key: 'question'
includeInJSON: 'id'
]
Answer:
class Entities.Answer extends App.Entities.Model
urlRoot: "answers"
defaults:
branching: false
next_question_id: null
Then when you go to display them, in my survey display view I have a layout view that has a question region which uses a composite view of the survey questions like this:
class Show.Controller extends App.Controllers.Application
initialize: (options) ->
{ survey, id } = options
survey or= App.request "survey:entity", id
App.execute "when:fetched", survey, =>
#layout = #getLayoutView()
#listenTo #layout, "show", =>
#panelRegion survey
#questionRegion survey
#bannerRegion survey
#show #layout
questionRegion: (survey) ->
App.request "show:survey:questions", survey, #layout.questionRegion
Then I come in and get the questions:
questionRegion: (survey) ->
questions = survey.get('questions')
questionView = #getQuestionView questions, survey
The childview of the Questions CompositeView is itself a CompositeView with a childview of answers.
So Survey has a Questions CompositeView of Questions, each of which is a CompositeView of Answers.
You should be able to follow a similar structure with your app. Let me know if you get stuck anywhere!
Edit: Adding View/Controllers.
So here's what I do, when the user navigates to a certain route - say localhost:3000/#surveys/1/edit it hits my surveysrouter (note some code like the list piece I stripped out):
#TheoremReach.module "SurveysApp", (SurveysApp, App, Backbone, Marionette, $, _) ->
class SurveysApp.Router extends Marionette.AppRouter
appRoutes:
"surveys" : "list"
"surveys/:id" : "show"
"surveys/:id/take": "take"
API =
show: (id, survey) ->
new SurveysApp.Show.Controller
id: id
survey: survey
take: (id, survey) ->
new SurveysApp.Take.Controller
id: id
survey: survey
App.vent.on "survey:clicked", (survey) ->
App.navigate "surveys/" + survey.id
API.show survey.id, survey
App.vent.on "take:survey:button:clicked", (survey) ->
App.navigate "surveys/" + survey.id + "/take"
API.take survey.id, survey
App.addInitializer ->
new SurveysApp.Router
controller: API
So I can get here when navigating or by triggering the "survey:clicked" event. This then creates my show controller:
#TheoremReach.module "SurveysApp.Show", (Show, App, Backbone, Marionette, $, _) ->
class Show.Controller extends App.Controllers.Application
initialize: (options) ->
{ survey, id } = options
survey or= App.request "survey:entity", id
App.execute "when:fetched", survey, =>
#layout = #getLayoutView()
#listenTo #layout, "show", =>
#panelRegion survey
#questionRegion survey
#bannerRegion survey
#show #layout
questionRegion: (survey) ->
App.request "show:survey:questions", survey, #layout.questionRegion
panelRegion: (survey) ->
panelView = #getPanelView survey
#listenTo panelView, "new:question:clicked", (args) ->
question = App.request "new:question:entity"
model = args.model
model.get('questions').add(question)
question.set(survey_id: model.get('id'))
App.request "new:question:added"
#show panelView, region: #layout.panelRegion
bannerRegion: (survey) ->
bannerView = #getBannerView survey
#listenTo bannerView, "take:survey:button:clicked", (args) ->
App.vent.trigger "take:survey:button:clicked", args.model
#show bannerView, region: #layout.bannerRegion
getLayoutView: ->
new Show.Layout
getBannerView: (survey) ->
new Show.Banner
model: survey
getPanelView: (survey) ->
new Show.Panel
model: survey
This makes a new Questions Show Controller (same router case as above that handles "show:survey:questions" request and instigates a new controller so I'll skip that code).
#TheoremReach.module "QuestionsApp.Show", (Show, App, Backbone, Marionette, $, _) ->
class Show.Controller extends App.Controllers.Application
initialize: (options) ->
{ survey } = options
#layout = #getLayoutView()
#listenTo #layout, "show", =>
#questionRegion survey
#show #layout
questionRegion: (survey) ->
questions = survey.get('questions')
questionView = #getQuestionView questions, survey
App.reqres.setHandler "new:question:added", ->
questionView.render()
#show questionView, region: #layout.questionRegion
getLayoutView: ->
new Show.Layout
getQuestionView: (questions, survey) ->
new Show.Questions
collection: questions
model: survey
Standard composite view for the questions:
class Show.Questions extends App.Views.CompositeView
template: "questions/show/_questions"
className: "questions"
itemViewContainer: ".editor"
itemView: Show.Question
Then each question is a composite view:
class Show.Question extends App.Views.CompositeView
template: "questions/show/_question"
id: "1000"
className: "step"
initialize: ->
#collection = #model.get("answers")
#model.set(question_number: #model.collection.indexOf(#model) + 1)
if #model.get('free_text') and #model.get('answers').length < 1
answer = App.request "new:answer:entity"
answer.set(free_text: true, question: #model, title: #model.get('title'))
#collection.reset(answer, {silent: true})
#on "childview:answer:delete:clicked", (child, args) =>
args.collection = #model.get('answers')
#trigger "answer:delete:clicked", args
itemView: Show.Answer
itemViewContainer: ".answer-container"
It gets its collection from the answers group from backbone relational. I would note though that this probably should just be a layout and in the initialize function I should send a request to the answers app to get a list of answers and add those to the answer region. I just haven't gotten around to that yet :).

Getting all tags from my different records

I use EntityFramework on my ASP.NET MVC project.
Let's say I have the entity below:
public class Project
{
public int ProjectID { get; set; }
public string Description { get; set; }
public string Tags { get; set; }
}
Lets say I have the following data in my DB:
ProjectID: 1
Description: "My first element"
Tags: "one, three, five, seven"
ProjectID: 2
Description: "My second element"
Tags: "one, two, three, six"
ProjectID: 3
Description: "My third element"
Tags: "two, three, four"
I would like to collect all tags from all my records. So: "one, two, three, four, five, six, seven"
How can I do? This may seems a stupid question but I don't know how to proceed.
Thanks.
You need to use string.Split() to dig out each tag in your list.
HashSet<string> allTags = new HashSet<string>();
foreach(Project project in context.Projects)
{
string tagsList = project.Tags;
string[] separateTags = tagsList.Split(", ", StringSplitOptions.RemoveEmptyEntries);
foreach(string separateTag in separateTags)
{
allTags.Add(separateTag);
}
}
then allTags will contain all your tags. If you want to put them in one big string again, use string.Join.
After splitting the strings you can use SelectMany() to concatenate the collections, and then use Distinct() to remove duplicates.
var tags = context.Projects
.SelectMany(p => p.Tags.Split(", ", StringSplitOptions.RemoveEmptyEntries))
.Distinct();
Here is the query syntax version, which gets transated to a SelectMany() statement behind the scenes:
var tags = (from p in project
from tag in p.Tags.Split(", ", StringSplitOptions.RemoveEmptyEntries)
select tag).Distinct();
Unfortunately, Split() won't translate to SQL, so you have to do that in memory. I'd recommend the following:
var tags = context.Projects
// pull only the tags string into memory
.Select(p => p.Tags)
// operate in memory from this point on
.AsEnumerable()
// remove empty entries so you don't get "" interpreted as one empty tag
.SelectMany(tags => tags.Split(",", StringSplitOptions.RemoveEmptyEntries))
// assuming you don't want leading or trailing whitespace
.Select(tag => tag.Trim())
.ToList();

Resources